diff --git a/AGENTS.md b/AGENTS.md
index f08f3388..1dfc7462 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -294,7 +294,34 @@ Edit files in `inst/prompts/`. These are assembled in `btw_client()` based on co
- **Tool functions return `tool_result()` objects** with value and optional display metadata
- **All tool wrappers accept `_intent` parameter** (added automatically by `wrap_with_intent()`)
- **Snapshot tests for tool outputs** to catch formatting changes
-- **Use `check_*()` functions** from rlang for argument validation
+- **Use `check_*()` functions** argument validation, described below
+
+### Type Check Functions
+
+#### Scalars
+- `check_bool()` - Validates single TRUE/FALSE value
+- `check_string()` - Validates single string (allows empty by default)
+- `check_name()` - Validates non-empty string for use as name
+- `check_number_decimal()` - Validates single numeric value (allows decimals)
+- `check_number_whole()` - Validates single integer value
+- `check_symbol()` - Validates R symbol/name object
+- `check_arg()` - Validates argument name (symbol)
+- `check_call()` - Validates defused call object
+- `check_environment()` - Validates environment object
+- `check_function()` - Validates function (any type)
+- `check_closure()` - Validates R function (not primitive/builtin)
+- `check_formula()` - Validates formula object
+
+#### Vectors
+- `check_character()` - Validates character vector
+- `check_logical()` - Validates logical vector
+- `check_data_frame()` - Validates data frame
+
+#### Common Parameters
+- All functions support `allow_null` parameter
+- Most scalars support `allow_na`
+- Number checkers support `min`/`max` bounds and `allow_infinite`
+- All use `arg`/`call` for error context
## Important Notes
diff --git a/DESCRIPTION b/DESCRIPTION
index 32ecf853..1eb393d2 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -82,6 +82,7 @@ Collate:
'tool-environment.R'
'tool-files-code-search.R'
'tool-files.R'
+ 'tool-git.R'
'tool-rstudioapi.R'
'tool-search-packages.R'
'tool-session-package-installed.R'
diff --git a/NAMESPACE b/NAMESPACE
index 9bf5001c..44437cc7 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -38,6 +38,13 @@ export(btw_tool_files_code_search)
export(btw_tool_files_list_files)
export(btw_tool_files_read_text_file)
export(btw_tool_files_write_text_file)
+export(btw_tool_git_branch_checkout)
+export(btw_tool_git_branch_create)
+export(btw_tool_git_branch_list)
+export(btw_tool_git_commit)
+export(btw_tool_git_diff)
+export(btw_tool_git_log)
+export(btw_tool_git_status)
export(btw_tool_ide_read_current_editor)
export(btw_tool_search_package_info)
export(btw_tool_search_packages)
diff --git a/R/btw_client_app.R b/R/btw_client_app.R
index db77a993..384bb4ff 100644
--- a/R/btw_client_app.R
+++ b/R/btw_client_app.R
@@ -164,6 +164,29 @@ btw_app_from_client <- function(client, messages = list(), ...) {
}
})
+ lapply(tool_groups, function(group) {
+ shiny::observeEvent(input[[paste0("tools_toggle_", group)]], {
+ current <- input[[paste0("tools_", group)]]
+ all_tools <- btw_tools_df()
+ group_tools <- all_tools[all_tools$group == group, ][["name"]]
+ if (length(current) == length(group_tools)) {
+ # All selected, so deselect all
+ shiny::updateCheckboxGroupInput(
+ session = session,
+ inputId = paste0("tools_", group),
+ selected = ""
+ )
+ } else {
+ # Not all selected, so select all
+ shiny::updateCheckboxGroupInput(
+ session = session,
+ inputId = paste0("tools_", group),
+ selected = group_tools
+ )
+ }
+ })
+ })
+
shiny::observe({
if (!length(selected_tools())) {
client$set_tools(list())
@@ -256,34 +279,33 @@ app_tool_group_choice_input <- function(
initial_tool_names <- group_tools_df$name
}
- label_icon <- switch(
- group,
- "docs" = tool_icon("dictionary"),
- "env" = tool_icon("source-environment"),
- "files" = tool_icon("folder-open"),
- "ide" = tool_icon("code-blocks"),
- "search" = tool_icon("search"),
- "session" = tool_icon("screen-search-desktop"),
- "web" = tool_icon("globe-book"),
- tool_icon("construction")
- )
+ label_icon <- tool_group_icon(group, "construction")
label_text <- switch(
group,
"docs" = shiny::span(label_icon, "Documentation"),
"env" = shiny::span(label_icon, "Environment"),
"files" = shiny::span(label_icon, "Files"),
+ "git" = shiny::span(label_icon, "Git"),
"ide" = shiny::span(label_icon, "IDE"),
"search" = shiny::span(label_icon, "Search"),
"session" = shiny::span(label_icon, "Session Info"),
"web" = shiny::span(label_icon, "Web Tools"),
"other" = shiny::span(label_icon, "Other Tools"),
- paste0(toupper(substring(group, 1, 1)), substring(group, 2))
+ to_title_case(group)
)
shiny::checkboxGroupInput(
inputId = paste0("tools_", group),
- label = shiny::h3(label_text, class = "h6 mb-0"),
+ label = shiny::h3(
+ class = "h6 mb-0",
+ shiny::actionLink(
+ paste0("tools_toggle_", group),
+ label_text,
+ class = "link-body-emphasis",
+ style = "text-decoration: none;"
+ )
+ ),
choiceNames = choice_names,
choiceValues = group_tools_df$name,
selected = intersect(group_tools_df$name, initial_tool_names),
diff --git a/R/edit_btw_md.R b/R/edit_btw_md.R
index 228c9789..7f18d233 100644
--- a/R/edit_btw_md.R
+++ b/R/edit_btw_md.R
@@ -110,7 +110,7 @@
#' ````
#' ---
#' client: claude/claude-4-5-sonnet-latest
-#' tools: [docs, env, files, ide, search, session, web]
+#' tools: [docs, env, files, git, ide, search, session, web]
#' ---
#'
#' Follow these important style rules when writing R code:
diff --git a/R/tool-git.R b/R/tool-git.R
new file mode 100644
index 00000000..5c5df267
--- /dev/null
+++ b/R/tool-git.R
@@ -0,0 +1,643 @@
+#' @include tool-result.R
+NULL
+
+# Git Status ------------------------------------------------------------------
+
+#' Tool: Git Status
+#'
+#' @examples
+#' \dontrun{
+#' btw_tool_git_status()
+#' }
+#'
+#' @param include One of `"both"`, `"staged"`, or `"unstaged"`. Use `"both"` to
+#' show both staged and unstaged files (default).
+#' @param pathspec Optional character vector with paths to match.
+#' @inheritParams btw_tool_docs_package_news
+#'
+#' @return Returns a character table of file statuses.
+#'
+#' @family git tools
+#' @export
+btw_tool_git_status <- function(include, pathspec, `_intent`) {}
+
+btw_tool_git_status_impl <- function(
+ include = c("both", "staged", "unstaged"),
+ pathspec = NULL
+) {
+ check_installed("gert")
+ include <- arg_match(include)
+ check_character(pathspec, allow_null = TRUE)
+
+ staged <- switch(
+ include,
+ both = NULL,
+ staged = TRUE,
+ unstaged = FALSE
+ )
+
+ status <- gert::git_status(staged = staged, pathspec = pathspec)
+
+ if (nrow(status) == 0) {
+ return(btw_tool_result("No changes to report"))
+ }
+
+ md_res <- glue_(
+ "{{ status$file }} [{{ status$status }}]{{ ifelse(status$staged, ' +staged', ' -unstaged') }}"
+ )
+
+ btw_tool_result(
+ paste(md_res, collapse = "\n"),
+ data = status,
+ display = list(markdown = paste("*", md_res, collapse = "\n"))
+ )
+}
+
+.btw_add_to_tools(
+ name = "btw_tool_git_status",
+ group = "git",
+ tool = function() {
+ ellmer::tool(
+ btw_tool_git_status_impl,
+ name = "btw_tool_git_status",
+ description = r"---(Show the status of the git working directory.
+
+WHEN TO USE:
+* Use this tool to see which files have been modified, staged, or are untracked.
+* This is typically the first tool to call when working with git operations.
+* If a staged file also has unstaged modifications, only the staged file and status are listed.
+
+RETURNS: A list of file paths, their status (new, modified, deleted, etc.), and whether they are staged or unstaged.
+ )---",
+ annotations = ellmer::tool_annotations(
+ title = "Git Status",
+ read_only_hint = TRUE,
+ open_world_hint = FALSE,
+ idempotent_hint = FALSE,
+ btw_can_register = function() rlang::is_installed("gert")
+ ),
+ arguments = list(
+ include = ellmer::type_enum(
+ c("both", "staged", "unstaged"),
+ 'Optional. One of "both" (default), "staged", or "unstaged" to filter status output.',
+ required = FALSE
+ ),
+ pathspec = ellmer::type_array(
+ "Optional. Character vector with paths to match.",
+ items = ellmer::type_string(),
+ required = FALSE
+ )
+ )
+ )
+ }
+)
+
+# Git Diff --------------------------------------------------------------------
+
+#' Tool: Git Diff
+#'
+#' @examples
+#' \dontrun{
+#' btw_tool_git_diff()
+#' btw_tool_git_diff(ref = "HEAD")
+#' }
+#'
+#' @inheritParams gert::git_diff_patch
+#' @inheritParams btw_tool_docs_package_news
+#'
+#' @return Returns a diff patch as a formatted string.
+#'
+#' @family git tools
+#' @export
+btw_tool_git_diff <- function(ref, `_intent`) {}
+
+btw_tool_git_diff_impl <- function(ref = NULL) {
+ check_installed("gert")
+ check_string(ref, allow_null = TRUE)
+
+ diff_patch <- gert::git_diff_patch(ref = ref)
+
+ if (length(diff_patch) == 0 || all(!nzchar(diff_patch))) {
+ msg <- if (is.null(ref)) {
+ "No unstaged changes to show"
+ } else {
+ sprintf("No changes to show for ref: %s", ref)
+ }
+ return(btw_tool_result(msg))
+ }
+
+ # Format as a code block
+ value <- md_code_block("diff", diff_patch)
+
+ btw_tool_result(
+ value,
+ display = list(
+ markdown = value,
+ title = HTML(sprintf(
+ "Git Diff%s",
+ if (!is.null(ref)) sprintf(" (%s)", ref) else ""
+ ))
+ )
+ )
+}
+
+.btw_add_to_tools(
+ name = "btw_tool_git_diff",
+ group = "git",
+ tool = function() {
+ ellmer::tool(
+ btw_tool_git_diff_impl,
+ name = "btw_tool_git_diff",
+ description = r"---(View changes in the working directory or a commit.
+
+WHEN TO USE:
+* Use with no arguments to see unstaged changes (working directory vs. index).
+* Use with `ref = "HEAD"` to see staged changes (index vs. HEAD).
+* Use with `ref = "commit_sha"` to see the diff of that commit.
+
+RETURNS: A unified diff patch showing the changes for a single commit.
+
+LIMITATION: This tool does not support diffing between two arbitrary commits.
+ )---",
+ annotations = ellmer::tool_annotations(
+ title = "Git Diff",
+ read_only_hint = TRUE,
+ open_world_hint = FALSE,
+ idempotent_hint = TRUE,
+ btw_can_register = function() rlang::is_installed("gert")
+ ),
+ arguments = list(
+ ref = ellmer::type_string(
+ 'Optional reference like "HEAD" or a commit id. `null` (default) shows unstaged changes.',
+ required = FALSE
+ )
+ )
+ )
+ }
+)
+
+# Git Log ---------------------------------------------------------------------
+
+#' Tool: Git Log
+#'
+#' @examples
+#' \dontrun{
+#' btw_tool_git_log()
+#' btw_tool_git_log(max = 20, ref = "main")
+#' }
+#'
+#' @param ref Revision string with a branch/tag/commit value. Defaults to
+#' `"HEAD"`.
+#' @param max Maximum number of commits to retrieve. Defaults to 10.
+#' @param after Optional date or timestamp: only include commits after this
+#' date.
+#' @inheritParams btw_tool_docs_package_news
+#'
+#' @return Returns a character table of commit history.
+#'
+#' @family git tools
+#' @export
+btw_tool_git_log <- function(ref, max, after, `_intent`) {}
+
+btw_tool_git_log_impl <- function(
+ ref = "HEAD",
+ max = 10,
+ after = NULL
+) {
+ check_installed("gert")
+ check_string(ref)
+ check_number_whole(max, min = 1)
+ check_string(after, allow_null = TRUE)
+
+ log <- gert::git_log(ref = ref, max = max, after = after)
+
+ if (nrow(log) == 0) {
+ return(btw_tool_result("No commits found"))
+ }
+
+ # Select and format relevant columns
+ names(log)[names(log) == "files"] <- "n_files"
+ fields <- c("message", "author", "time", "n_files", "commit")
+
+ # Truncate commit SHA to 7 characters for display
+ log_display <- log
+ log_display$commit <- substr(log_display$commit, 1, 7)
+
+ # Truncate long messages
+ log_display$message <- vapply(
+ log_display$message,
+ function(msg) {
+ first_line <- strsplit(msg, "\n")[[1]][1]
+ if (nchar(first_line) > 60) {
+ paste0(substr(first_line, 1, 57), "...")
+ } else {
+ first_line
+ }
+ },
+ character(1)
+ )
+
+ btw_tool_result(
+ md_kv_table(log_display[fields]),
+ data = log,
+ display = list(
+ markdown = md_table(log_display[rev(fields)])
+ )
+ )
+}
+
+.btw_add_to_tools(
+ name = "btw_tool_git_log",
+ group = "git",
+ tool = function() {
+ ellmer::tool(
+ btw_tool_git_log_impl,
+ name = "btw_tool_git_log",
+ description = r"---(Show the commit history for a repository.
+
+WHEN TO USE:
+* Use this tool to view recent commits and their messages.
+* Useful for understanding the history of changes before making new commits.
+* Can filter by branch, number of commits, or date range.
+
+RETURNS: A list of commits with SHA (short), author, timestamp, number of files, and message.
+ )---",
+ annotations = ellmer::tool_annotations(
+ title = "Git Log",
+ read_only_hint = TRUE,
+ open_world_hint = FALSE,
+ idempotent_hint = FALSE,
+ btw_can_register = function() rlang::is_installed("gert")
+ ),
+ arguments = list(
+ ref = ellmer::type_string(
+ 'Revision string (branch/tag/commit). Defaults to "HEAD".',
+ required = FALSE
+ ),
+ max = ellmer::type_number(
+ "Maximum number of commits to retrieve. Defaults to 10.",
+ required = FALSE
+ ),
+ after = ellmer::type_string(
+ "Optional date or timestamp: only include commits after this date.",
+ required = FALSE
+ )
+ )
+ )
+ }
+)
+
+# Git Commit ------------------------------------------------------------------
+
+#' Tool: Git Commit
+#'
+#' @examples
+#' \dontrun{
+#' btw_tool_git_commit(message = "Fix bug in analysis", files = c("analysis.R"))
+#' }
+#'
+#' @param message A commit message describing the changes.
+#' @param files Optional character vector of file paths to stage and commit.
+#' Use `"."` to stage all changed files. If `NULL`, commits currently staged files.
+#' @inheritParams btw_tool_docs_package_news
+#'
+#' @return Returns the commit SHA.
+#'
+#' @family git tools
+#' @export
+btw_tool_git_commit <- function(message, files, `_intent`) {}
+
+btw_tool_git_commit_impl <- function(
+ message,
+ files = NULL
+) {
+ check_installed("gert")
+ check_string(message)
+ check_character(files, allow_null = TRUE)
+
+ # Stage files if provided
+ if (!is.null(files)) {
+ gert::git_add(files)
+ }
+
+ # Create commit
+ commit_sha <- gert::git_commit(message = message)
+
+ result <- sprintf(
+ "Created commit: %s\nMessage: %s",
+ substr(commit_sha, 1, 7),
+ message
+ )
+
+ btw_tool_result(
+ result,
+ data = list(sha = commit_sha, message = message),
+ display = list(
+ markdown = md_code_block("", result),
+ title = "Git Commit"
+ )
+ )
+}
+
+.btw_add_to_tools(
+ name = "btw_tool_git_commit",
+ group = "git",
+ tool = function() {
+ ellmer::tool(
+ btw_tool_git_commit_impl,
+ name = "btw_tool_git_commit",
+ description = r"---(Stage files and create a git commit.
+
+WHEN TO USE:
+* Use this tool to commit changes.
+* If `files` is provided, those files will be staged before committing.
+* If `files` is NULL, only currently staged files will be committed.
+
+IMPORTANT:
+* Always provide a clear, descriptive commit message.
+* You can review changes with btw_tool_git_status and btw_tool_git_diff before committing.
+* This modifies the repository state.
+
+RETURNS: The commit SHA and confirmation message.
+ )---",
+ annotations = ellmer::tool_annotations(
+ title = "Git Commit",
+ read_only_hint = FALSE,
+ open_world_hint = FALSE,
+ idempotent_hint = FALSE,
+ btw_can_register = function() rlang::is_installed("gert")
+ ),
+ arguments = list(
+ message = ellmer::type_string(
+ "A commit message describing the changes."
+ ),
+ files = ellmer::type_array(
+ 'Optional. Files to stage before committing. Use "." for all changed files, or NULL to commit already staged files.',
+ items = ellmer::type_string(),
+ required = FALSE
+ )
+ )
+ )
+ }
+)
+
+# Git Branch List -------------------------------------------------------------
+
+#' Tool: Git Branch List
+#'
+#' @examples
+#' \dontrun{
+#' btw_tool_git_branch_list()
+#' btw_tool_git_branch_list(local = FALSE) # remote branches
+#' }
+#'
+#' @param include Once of `"local"` (default), `"remote"`, or `"all"` to filter
+#' branches to local branches only, remote branches only, or all branches.
+#' @inheritParams btw_tool_docs_package_news
+#'
+#' @return Returns a character table of branches.
+#'
+#' @family git tools
+#' @export
+btw_tool_git_branch_list <- function(include, `_intent`) {}
+
+btw_tool_git_branch_list_impl <- function(
+ include = c("local", "remote", "all")
+) {
+ check_installed("gert")
+ include <- arg_match(include)
+
+ local <- switch(include, local = TRUE, remote = FALSE, all = NULL)
+
+ branches <- gert::git_branch_list(local = local)
+
+ if (nrow(branches) == 0) {
+ return(btw_tool_result("No branches found"))
+ }
+
+ # Select relevant columns
+ fields <- c("name", "upstream", "updated")
+
+ branches <- branches[order(branches$updated, decreasing = TRUE), ]
+
+ branches_llm <- glue_(
+ "{{ branches$name }} [{{ branches$updated }}]{{ ifelse(!is.na(branches$upstream), paste(' ->', branches$upstream), '') }} "
+ )
+
+ btw_tool_result(
+ paste(branches_llm, collapse = "\n"),
+ data = branches,
+ display = list(markdown = md_table(branches[fields]))
+ )
+}
+
+.btw_add_to_tools(
+ name = "btw_tool_git_branch_list",
+ group = "git",
+ tool = function() {
+ ellmer::tool(
+ btw_tool_git_branch_list_impl,
+ name = "btw_tool_git_branch_list",
+ description = r"---(List git branches in the repository.
+
+WHEN TO USE:
+* Use this tool to see available branches before checking out or creating a new branch.
+* Shows local branches by default, but can also show remote branches.
+
+RETURNS: A table of branch names, upstream tracking, and last update time.
+ )---",
+ annotations = ellmer::tool_annotations(
+ title = "Git Branches",
+ read_only_hint = TRUE,
+ open_world_hint = FALSE,
+ idempotent_hint = TRUE,
+ btw_can_register = function() rlang::is_installed("gert")
+ ),
+ arguments = list(
+ include = ellmer::type_enum(
+ c("local", "remote", "all"),
+ 'Optional. Filter branches to "local" (default), "remote", or "all".',
+ required = FALSE
+ )
+ )
+ )
+ }
+)
+
+# Git Branch Create -----------------------------------------------------------
+
+#' Tool: Git Branch Create
+#'
+#' @examples
+#' \dontrun{
+#' btw_tool_git_branch_create(branch = "feature/new-analysis")
+#' }
+#'
+#' @param branch Name of the new branch to create.
+#' @param ref Optional reference point for the new branch. Defaults to `"HEAD"`.
+#' @param checkout Whether to check out the new branch after creation. Defaults
+#' to `TRUE`.
+#' @inheritParams btw_tool_docs_package_news
+#'
+#' @return Returns a confirmation message.
+#'
+#' @family git tools
+#' @export
+btw_tool_git_branch_create <- function(branch, ref, checkout, `_intent`) {}
+
+btw_tool_git_branch_create_impl <- function(
+ branch,
+ ref = "HEAD",
+ checkout = TRUE
+) {
+ check_installed("gert")
+ check_string(branch)
+ check_string(ref)
+ check_bool(checkout)
+
+ gert::git_branch_create(branch = branch, ref = ref, checkout = checkout)
+
+ result <- sprintf(
+ "Created branch `%s` from `%s`%s.",
+ branch,
+ ref,
+ if (checkout) " and checked it out" else ""
+ )
+
+ btw_tool_result(
+ result,
+ display = list(
+ markdown = result,
+ title = HTML(sprintf(
+ "Git Create Branch %s",
+ branch
+ ))
+ )
+ )
+}
+
+.btw_add_to_tools(
+ name = "btw_tool_git_branch_create",
+ group = "git",
+ tool = function() {
+ ellmer::tool(
+ btw_tool_git_branch_create_impl,
+ name = "btw_tool_git_branch_create",
+ description = r"---(Create a new git branch.
+
+WHEN TO USE:
+* Use this tool to create a new branch for feature development or bug fixes.
+* By default, the new branch is checked out automatically.
+* The branch is created from HEAD unless a different ref is specified.
+
+IMPORTANT:
+* This modifies the repository state.
+* Ensure you're on the correct branch before creating from HEAD.
+
+RETURNS: Confirmation message with branch name and ref.
+ )---",
+ annotations = ellmer::tool_annotations(
+ title = "Git Branch Create",
+ read_only_hint = FALSE,
+ open_world_hint = FALSE,
+ idempotent_hint = FALSE,
+ btw_can_register = function() rlang::is_installed("gert")
+ ),
+ arguments = list(
+ branch = ellmer::type_string(
+ "Name of the new branch to create."
+ ),
+ ref = ellmer::type_string(
+ 'Optional reference point (branch/tag/commit) for the new branch. Defaults to "HEAD".',
+ required = FALSE
+ ),
+ checkout = ellmer::type_boolean(
+ "Whether to check out the new branch after creation. Defaults to TRUE.",
+ required = FALSE
+ )
+ )
+ )
+ }
+)
+
+# Git Branch Checkout ---------------------------------------------------------
+
+#' Tool: Git Branch Checkout
+#'
+#' @examples
+#' \dontrun{
+#' btw_tool_git_branch_checkout(branch = "main")
+#' }
+#'
+#' @param branch Name of branch to check out.
+#' @param force Whether to force checkout even with uncommitted changes.
+#' Defaults to `FALSE`.
+#' @inheritParams btw_tool_docs_package_news
+#'
+#' @return Returns a confirmation message.
+#'
+#' @family git tools
+#' @export
+btw_tool_git_branch_checkout <- function(branch, force, `_intent`) {}
+
+btw_tool_git_branch_checkout_impl <- function(
+ branch,
+ force = FALSE
+) {
+ check_installed("gert")
+ check_string(branch)
+ check_bool(force)
+
+ gert::git_branch_checkout(branch = branch, force = force)
+
+ result <- sprintf("Checked out branch '%s'", branch)
+
+ btw_tool_result(
+ result,
+ display = list(
+ markdown = md_code_block("", result),
+ title = HTML("Git Checkout")
+ )
+ )
+}
+
+.btw_add_to_tools(
+ name = "btw_tool_git_branch_checkout",
+ group = "git",
+ tool = function() {
+ ellmer::tool(
+ btw_tool_git_branch_checkout_impl,
+ name = "btw_tool_git_branch_checkout",
+ description = r"---(Switch to a different git branch.
+
+WHEN TO USE:
+* Use this tool to switch between existing branches.
+* Check btw_tool_git_status first to ensure no uncommitted changes will be lost.
+* Use btw_tool_git_branch_list to see available branches.
+
+IMPORTANT:
+* This modifies the repository state and working directory.
+* Will fail if there are uncommitted changes unless force is true.
+* Using `force: true` can lose uncommitted changes.
+
+RETURNS: Confirmation message with branch name.
+ )---",
+ annotations = ellmer::tool_annotations(
+ title = "Git Checkout",
+ read_only_hint = FALSE,
+ open_world_hint = FALSE,
+ idempotent_hint = FALSE,
+ btw_can_register = function() rlang::is_installed("gert")
+ ),
+ arguments = list(
+ branch = ellmer::type_string(
+ "Name of the branch to check out."
+ ),
+ force = ellmer::type_boolean(
+ "Whether to force checkout even with uncommitted changes. Defaults to false. Use with caution.",
+ required = FALSE
+ )
+ )
+ )
+ }
+)
diff --git a/R/tools.R b/R/tools.R
index c069a687..2f817b87 100644
--- a/R/tools.R
+++ b/R/tools.R
@@ -132,24 +132,27 @@ wrap_with_intent <- function(tool) {
tool
}
-set_tool_icon <- function(tool, group) {
- icon <- switch(
+tool_group_icon <- function(group, default = NULL) {
+ switch(
group,
"docs" = tool_icon("dictionary"),
"env" = tool_icon("source-environment"),
"files" = tool_icon("folder-open"),
+ "git" = tool_icon("difference"),
"ide" = tool_icon("code-blocks"),
"search" = tool_icon("search"),
"session" = tool_icon("screen-search-desktop"),
"web" = tool_icon("globe-book"),
- NULL
+ if (!is.null(default)) tool_icon(default)
)
+}
+set_tool_icon <- function(tool, group) {
if (!is.list(tool@annotations)) {
tool@annotations <- list()
}
- tool@annotations$icon <- icon
+ tool@annotations$icon <- tool_group_icon(group)
tool
}
@@ -188,6 +191,14 @@ tool_icon <- local({
})
x <- do.call(rbind, x)
x <- x[order(x$Group, x$Name), ]
- md_table(x)
+ x$Name <- sprintf("[%s()]", x$Name)
+
+ res <- c()
+ for (group in unique(x$Group)) {
+ res <- c(res, paste0("### Group: ", group, "\n\n"))
+ res <- c(res, md_table(x[x$Group == group, c("Name", "Description")]))
+ res <- c(res, "\n\n")
+ }
+ paste(res, collapse = "\n")
}
# nocov end
diff --git a/R/utils-md.R b/R/utils-md.R
index 697aa9ec..d4718f6b 100644
--- a/R/utils-md.R
+++ b/R/utils-md.R
@@ -25,3 +25,24 @@ md_code_block <- function(type = "", ...) {
}
c(paste0(ticks, type), ..., ticks)
}
+
+md_kv_table <- function(df, drop_na = FALSE) {
+ check_data_frame(df)
+
+ res <- vector("character", nrow(df))
+
+ for (i in seq_len(nrow(df))) {
+ item <- vector("character", ncol(df))
+ for (j in seq_len(ncol(df))) {
+ value <- df[[j]][[i]]
+ if (drop_na && is.na(value)) {
+ next
+ }
+ item[j] <- paste0(colnames(df)[j], ": ", format(value))
+ }
+ item <- item[nzchar(item)]
+ res[i] <- paste(item, collapse = "\n")
+ }
+
+ paste(res, collapse = "\n\n")
+}
diff --git a/R/utils.R b/R/utils.R
index 9e17ff0b..2e7ab3ae 100644
--- a/R/utils.R
+++ b/R/utils.R
@@ -162,3 +162,7 @@ local_reproducible_output <- function(
.local_envir = .env
)
}
+
+to_title_case <- function(x) {
+ paste0(toupper(substring(x, 1, 1)), substring(x, 2))
+}
diff --git a/inst/icons/difference.svg b/inst/icons/difference.svg
new file mode 100644
index 00000000..ef17e661
--- /dev/null
+++ b/inst/icons/difference.svg
@@ -0,0 +1 @@
+
diff --git a/inst/templates/btw.md b/inst/templates/btw.md
index 21d76305..5b062268 100644
--- a/inst/templates/btw.md
+++ b/inst/templates/btw.md
@@ -1,6 +1,14 @@
---
client: claude/claude-4-5-sonnet-latest
-tools: [docs, env, files, ide, search, session, web]
+tools:
+ - docs
+ - env
+ - files
+ - git
+ - ide
+ - search
+ - session
+ - web
---
Follow these important style rules when writing R code:
diff --git a/man/btw_tool_git_branch_checkout.Rd b/man/btw_tool_git_branch_checkout.Rd
new file mode 100644
index 00000000..5094c62a
--- /dev/null
+++ b/man/btw_tool_git_branch_checkout.Rd
@@ -0,0 +1,40 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tool-git.R
+\name{btw_tool_git_branch_checkout}
+\alias{btw_tool_git_branch_checkout}
+\title{Tool: Git Branch Checkout}
+\usage{
+btw_tool_git_branch_checkout(branch, force = FALSE, `_intent` = "")
+}
+\arguments{
+\item{branch}{Name of branch to check out.}
+
+\item{force}{Whether to force checkout even with uncommitted changes.
+Defaults to \code{FALSE}.}
+
+\item{_intent}{An optional string describing the intent of the tool use.
+When the tool is used by an LLM, the model will use this argument to
+explain why it called the tool.}
+}
+\value{
+Returns a confirmation message.
+}
+\description{
+Tool: Git Branch Checkout
+}
+\examples{
+\dontrun{
+btw_tool_git_branch_checkout(branch = "main")
+}
+
+}
+\seealso{
+Other git tools:
+\code{\link{btw_tool_git_branch_create}()},
+\code{\link{btw_tool_git_branch_list}()},
+\code{\link{btw_tool_git_commit}()},
+\code{\link{btw_tool_git_diff}()},
+\code{\link{btw_tool_git_log}()},
+\code{\link{btw_tool_git_status}()}
+}
+\concept{git tools}
diff --git a/man/btw_tool_git_branch_create.Rd b/man/btw_tool_git_branch_create.Rd
new file mode 100644
index 00000000..0f18db6b
--- /dev/null
+++ b/man/btw_tool_git_branch_create.Rd
@@ -0,0 +1,47 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tool-git.R
+\name{btw_tool_git_branch_create}
+\alias{btw_tool_git_branch_create}
+\title{Tool: Git Branch Create}
+\usage{
+btw_tool_git_branch_create(
+ branch,
+ ref = "HEAD",
+ checkout = TRUE,
+ `_intent` = ""
+)
+}
+\arguments{
+\item{branch}{Name of the new branch to create.}
+
+\item{ref}{Optional reference point for the new branch. Defaults to \code{"HEAD"}.}
+
+\item{checkout}{Whether to check out the new branch after creation. Defaults
+to \code{TRUE}.}
+
+\item{_intent}{An optional string describing the intent of the tool use.
+When the tool is used by an LLM, the model will use this argument to
+explain why it called the tool.}
+}
+\value{
+Returns a confirmation message.
+}
+\description{
+Tool: Git Branch Create
+}
+\examples{
+\dontrun{
+btw_tool_git_branch_create(branch = "feature/new-analysis")
+}
+
+}
+\seealso{
+Other git tools:
+\code{\link{btw_tool_git_branch_checkout}()},
+\code{\link{btw_tool_git_branch_list}()},
+\code{\link{btw_tool_git_commit}()},
+\code{\link{btw_tool_git_diff}()},
+\code{\link{btw_tool_git_log}()},
+\code{\link{btw_tool_git_status}()}
+}
+\concept{git tools}
diff --git a/man/btw_tool_git_branch_list.Rd b/man/btw_tool_git_branch_list.Rd
new file mode 100644
index 00000000..6539d21b
--- /dev/null
+++ b/man/btw_tool_git_branch_list.Rd
@@ -0,0 +1,39 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tool-git.R
+\name{btw_tool_git_branch_list}
+\alias{btw_tool_git_branch_list}
+\title{Tool: Git Branch List}
+\usage{
+btw_tool_git_branch_list(include = c("local", "remote", "all"), `_intent` = "")
+}
+\arguments{
+\item{include}{Once of \code{"local"} (default), \code{"remote"}, or \code{"all"} to filter
+branches to local branches only, remote branches only, or all branches.}
+
+\item{_intent}{An optional string describing the intent of the tool use.
+When the tool is used by an LLM, the model will use this argument to
+explain why it called the tool.}
+}
+\value{
+Returns a character table of branches.
+}
+\description{
+Tool: Git Branch List
+}
+\examples{
+\dontrun{
+btw_tool_git_branch_list()
+btw_tool_git_branch_list(local = FALSE) # remote branches
+}
+
+}
+\seealso{
+Other git tools:
+\code{\link{btw_tool_git_branch_checkout}()},
+\code{\link{btw_tool_git_branch_create}()},
+\code{\link{btw_tool_git_commit}()},
+\code{\link{btw_tool_git_diff}()},
+\code{\link{btw_tool_git_log}()},
+\code{\link{btw_tool_git_status}()}
+}
+\concept{git tools}
diff --git a/man/btw_tool_git_commit.Rd b/man/btw_tool_git_commit.Rd
new file mode 100644
index 00000000..55b1a214
--- /dev/null
+++ b/man/btw_tool_git_commit.Rd
@@ -0,0 +1,40 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tool-git.R
+\name{btw_tool_git_commit}
+\alias{btw_tool_git_commit}
+\title{Tool: Git Commit}
+\usage{
+btw_tool_git_commit(message, files = NULL, `_intent` = "")
+}
+\arguments{
+\item{message}{A commit message describing the changes.}
+
+\item{files}{Optional character vector of file paths to stage and commit.
+Use \code{"."} to stage all changed files. If \code{NULL}, commits currently staged files.}
+
+\item{_intent}{An optional string describing the intent of the tool use.
+When the tool is used by an LLM, the model will use this argument to
+explain why it called the tool.}
+}
+\value{
+Returns the commit SHA.
+}
+\description{
+Tool: Git Commit
+}
+\examples{
+\dontrun{
+btw_tool_git_commit(message = "Fix bug in analysis", files = c("analysis.R"))
+}
+
+}
+\seealso{
+Other git tools:
+\code{\link{btw_tool_git_branch_checkout}()},
+\code{\link{btw_tool_git_branch_create}()},
+\code{\link{btw_tool_git_branch_list}()},
+\code{\link{btw_tool_git_diff}()},
+\code{\link{btw_tool_git_log}()},
+\code{\link{btw_tool_git_status}()}
+}
+\concept{git tools}
diff --git a/man/btw_tool_git_diff.Rd b/man/btw_tool_git_diff.Rd
new file mode 100644
index 00000000..375405a9
--- /dev/null
+++ b/man/btw_tool_git_diff.Rd
@@ -0,0 +1,39 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tool-git.R
+\name{btw_tool_git_diff}
+\alias{btw_tool_git_diff}
+\title{Tool: Git Diff}
+\usage{
+btw_tool_git_diff(ref = NULL, `_intent` = "")
+}
+\arguments{
+\item{ref}{a reference such as \code{"HEAD"}, or a commit id, or \code{NULL}
+to the diff the working directory against the repository index.}
+
+\item{_intent}{An optional string describing the intent of the tool use.
+When the tool is used by an LLM, the model will use this argument to
+explain why it called the tool.}
+}
+\value{
+Returns a diff patch as a formatted string.
+}
+\description{
+Tool: Git Diff
+}
+\examples{
+\dontrun{
+btw_tool_git_diff()
+btw_tool_git_diff(ref = "HEAD")
+}
+
+}
+\seealso{
+Other git tools:
+\code{\link{btw_tool_git_branch_checkout}()},
+\code{\link{btw_tool_git_branch_create}()},
+\code{\link{btw_tool_git_branch_list}()},
+\code{\link{btw_tool_git_commit}()},
+\code{\link{btw_tool_git_log}()},
+\code{\link{btw_tool_git_status}()}
+}
+\concept{git tools}
diff --git a/man/btw_tool_git_log.Rd b/man/btw_tool_git_log.Rd
new file mode 100644
index 00000000..b4f53645
--- /dev/null
+++ b/man/btw_tool_git_log.Rd
@@ -0,0 +1,44 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tool-git.R
+\name{btw_tool_git_log}
+\alias{btw_tool_git_log}
+\title{Tool: Git Log}
+\usage{
+btw_tool_git_log(ref = "HEAD", max = 10, after = NULL, `_intent` = "")
+}
+\arguments{
+\item{ref}{Revision string with a branch/tag/commit value. Defaults to
+\code{"HEAD"}.}
+
+\item{max}{Maximum number of commits to retrieve. Defaults to 10.}
+
+\item{after}{Optional date or timestamp: only include commits after this
+date.}
+
+\item{_intent}{An optional string describing the intent of the tool use.
+When the tool is used by an LLM, the model will use this argument to
+explain why it called the tool.}
+}
+\value{
+Returns a character table of commit history.
+}
+\description{
+Tool: Git Log
+}
+\examples{
+\dontrun{
+btw_tool_git_log()
+btw_tool_git_log(max = 20, ref = "main")
+}
+
+}
+\seealso{
+Other git tools:
+\code{\link{btw_tool_git_branch_checkout}()},
+\code{\link{btw_tool_git_branch_create}()},
+\code{\link{btw_tool_git_branch_list}()},
+\code{\link{btw_tool_git_commit}()},
+\code{\link{btw_tool_git_diff}()},
+\code{\link{btw_tool_git_status}()}
+}
+\concept{git tools}
diff --git a/man/btw_tool_git_status.Rd b/man/btw_tool_git_status.Rd
new file mode 100644
index 00000000..e7506362
--- /dev/null
+++ b/man/btw_tool_git_status.Rd
@@ -0,0 +1,44 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tool-git.R
+\name{btw_tool_git_status}
+\alias{btw_tool_git_status}
+\title{Tool: Git Status}
+\usage{
+btw_tool_git_status(
+ include = c("both", "staged", "unstaged"),
+ pathspec = NULL,
+ `_intent` = ""
+)
+}
+\arguments{
+\item{include}{One of \code{"both"}, \code{"staged"}, or \code{"unstaged"}. Use \code{"both"} to
+show both staged and unstaged files (default).}
+
+\item{pathspec}{Optional character vector with paths to match.}
+
+\item{_intent}{An optional string describing the intent of the tool use.
+When the tool is used by an LLM, the model will use this argument to
+explain why it called the tool.}
+}
+\value{
+Returns a character table of file statuses.
+}
+\description{
+Tool: Git Status
+}
+\examples{
+\dontrun{
+btw_tool_git_status()
+}
+
+}
+\seealso{
+Other git tools:
+\code{\link{btw_tool_git_branch_checkout}()},
+\code{\link{btw_tool_git_branch_create}()},
+\code{\link{btw_tool_git_branch_list}()},
+\code{\link{btw_tool_git_commit}()},
+\code{\link{btw_tool_git_diff}()},
+\code{\link{btw_tool_git_log}()}
+}
+\concept{git tools}
diff --git a/man/btw_tools.Rd b/man/btw_tools.Rd
index be8046c6..bb420c04 100644
--- a/man/btw_tools.Rd
+++ b/man/btw_tools.Rd
@@ -29,26 +29,78 @@ place. The \code{chat} input is returned invisibly.
The \code{btw_tools()} function provides a list of tools that can be registered
with an ellmer chat via \code{chat$set_tools()} that allow the chat to
interface with your computational environment. Chats returned by
-this function have access to the tools:\tabular{lll}{
- Name \tab Group \tab Description \cr
- btw_tool_docs_available_vignettes \tab docs \tab List available vignettes for an R package. \cr
- btw_tool_docs_help_page \tab docs \tab Get help page from package. \cr
- btw_tool_docs_package_help_topics \tab docs \tab Get available help topics for an R package. \cr
- btw_tool_docs_package_news \tab docs \tab Read the release notes (NEWS) for a package. \cr
- btw_tool_docs_vignette \tab docs \tab Get a package vignette in plain text. \cr
- btw_tool_env_describe_data_frame \tab env \tab Show the data frame or table or get information about the structure of a data frame or table. \cr
- btw_tool_env_describe_environment \tab env \tab List and describe items in an environment. \cr
- btw_tool_files_code_search \tab files \tab Search code files in the project. \cr
- btw_tool_files_list_files \tab files \tab List files or directories in the project. \cr
- btw_tool_files_read_text_file \tab files \tab Read an entire text file. \cr
- btw_tool_files_write_text_file \tab files \tab Write content to a text file. \cr
- btw_tool_ide_read_current_editor \tab ide \tab Read the contents of the editor that is currently open in the user's IDE. \cr
- btw_tool_search_package_info \tab search \tab Describe a CRAN package. \cr
- btw_tool_search_packages \tab search \tab Search for an R package on CRAN. \cr
- btw_tool_session_check_package_installed \tab session \tab Check if a package is installed in the current session. \cr
- btw_tool_session_package_info \tab session \tab Verify that a specific package is installed, or find out which packages are in use in the current session. \cr
- btw_tool_session_platform_info \tab session \tab Describes the R version, operating system, language and locale settings for the user's system. \cr
- btw_tool_web_read_url \tab web \tab Read a web page and convert it to Markdown format. \cr
+this function have access to the tools:
+\subsection{Group: docs}{\tabular{ll}{
+ Name \tab Description \cr
+ \code{\link[=btw_tool_docs_available_vignettes]{btw_tool_docs_available_vignettes()}} \tab List available vignettes for an R package. \cr
+ \code{\link[=btw_tool_docs_help_page]{btw_tool_docs_help_page()}} \tab Get help page from package. \cr
+ \code{\link[=btw_tool_docs_package_help_topics]{btw_tool_docs_package_help_topics()}} \tab Get available help topics for an R package. \cr
+ \code{\link[=btw_tool_docs_package_news]{btw_tool_docs_package_news()}} \tab Read the release notes (NEWS) for a package. \cr
+ \code{\link[=btw_tool_docs_vignette]{btw_tool_docs_vignette()}} \tab Get a package vignette in plain text. \cr
+}
+
+}
+
+\subsection{Group: env}{\tabular{ll}{
+ Name \tab Description \cr
+ \code{\link[=btw_tool_env_describe_data_frame]{btw_tool_env_describe_data_frame()}} \tab Show the data frame or table or get information about the structure of a data frame or table. \cr
+ \code{\link[=btw_tool_env_describe_environment]{btw_tool_env_describe_environment()}} \tab List and describe items in an environment. \cr
+}
+
+}
+
+\subsection{Group: files}{\tabular{ll}{
+ Name \tab Description \cr
+ \code{\link[=btw_tool_files_code_search]{btw_tool_files_code_search()}} \tab Search code files in the project. \cr
+ \code{\link[=btw_tool_files_list_files]{btw_tool_files_list_files()}} \tab List files or directories in the project. \cr
+ \code{\link[=btw_tool_files_read_text_file]{btw_tool_files_read_text_file()}} \tab Read an entire text file. \cr
+ \code{\link[=btw_tool_files_write_text_file]{btw_tool_files_write_text_file()}} \tab Write content to a text file. \cr
+}
+
+}
+
+\subsection{Group: git}{\tabular{ll}{
+ Name \tab Description \cr
+ \code{\link[=btw_tool_git_branch_checkout]{btw_tool_git_branch_checkout()}} \tab Switch to a different git branch. \cr
+ \code{\link[=btw_tool_git_branch_create]{btw_tool_git_branch_create()}} \tab Create a new git branch. \cr
+ \code{\link[=btw_tool_git_branch_list]{btw_tool_git_branch_list()}} \tab List git branches in the repository. \cr
+ \code{\link[=btw_tool_git_commit]{btw_tool_git_commit()}} \tab Stage files and create a git commit. \cr
+ \code{\link[=btw_tool_git_diff]{btw_tool_git_diff()}} \tab View changes in the working directory or a commit. \cr
+ \code{\link[=btw_tool_git_log]{btw_tool_git_log()}} \tab Show the commit history for a repository. \cr
+ \code{\link[=btw_tool_git_status]{btw_tool_git_status()}} \tab Show the status of the git working directory. \cr
+}
+
+}
+
+\subsection{Group: ide}{\tabular{ll}{
+ Name \tab Description \cr
+ \code{\link[=btw_tool_ide_read_current_editor]{btw_tool_ide_read_current_editor()}} \tab Read the contents of the editor that is currently open in the user's IDE. \cr
+}
+
+}
+
+\subsection{Group: search}{\tabular{ll}{
+ Name \tab Description \cr
+ \code{\link[=btw_tool_search_package_info]{btw_tool_search_package_info()}} \tab Describe a CRAN package. \cr
+ \code{\link[=btw_tool_search_packages]{btw_tool_search_packages()}} \tab Search for an R package on CRAN. \cr
+}
+
+}
+
+\subsection{Group: session}{\tabular{ll}{
+ Name \tab Description \cr
+ \code{\link[=btw_tool_session_check_package_installed]{btw_tool_session_check_package_installed()}} \tab Check if a package is installed in the current session. \cr
+ \code{\link[=btw_tool_session_package_info]{btw_tool_session_package_info()}} \tab Verify that a specific package is installed, or find out which packages are in use in the current session. \cr
+ \code{\link[=btw_tool_session_platform_info]{btw_tool_session_platform_info()}} \tab Describes the R version, operating system, language and locale settings for the user's system. \cr
+}
+
+}
+
+\subsection{Group: web}{\tabular{ll}{
+ Name \tab Description \cr
+ \code{\link[=btw_tool_web_read_url]{btw_tool_web_read_url()}} \tab Read a web page and convert it to Markdown format. \cr
+}
+
}
}
\examples{
diff --git a/man/use_btw_md.Rd b/man/use_btw_md.Rd
index b19bf757..e5bfe94f 100644
--- a/man/use_btw_md.Rd
+++ b/man/use_btw_md.Rd
@@ -143,7 +143,7 @@ Here's an example \code{btw.md} file:
\if{html}{\out{