From 4e1eacb522b4e489575c3e9e7e617aacef461b8c Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 10:12:21 -0400 Subject: [PATCH 01/23] Add git tools for basic repository operations Implements 7 new git tools using the {gert} package: - btw_tool_git_status() - Show working directory status - btw_tool_git_diff() - View diffs between refs or working tree - btw_tool_git_log() - List commit history - btw_tool_git_commit() - Create commits - btw_tool_git_branch_list() - List branches - btw_tool_git_branch_create() - Create new branches - btw_tool_git_branch_checkout() - Switch branches Tools are conditionally registered only when {gert} is installed, with runtime checks using check_installed("gert"). Generated with Claude Code --- DESCRIPTION | 1 + R/tool-git.R | 626 +++++++++++++++++++++++++++++++++ R/tools.R | 1 + tests/testthat/test-tool-git.R | 322 +++++++++++++++++ 4 files changed, 950 insertions(+) create mode 100644 R/tool-git.R create mode 100644 tests/testthat/test-tool-git.R 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/R/tool-git.R b/R/tool-git.R new file mode 100644 index 00000000..4cbdddd6 --- /dev/null +++ b/R/tool-git.R @@ -0,0 +1,626 @@ +#' @include tool-result.R +NULL + +# Git Status ------------------------------------------------------------------ + +#' Tool: Git Status +#' +#' @examples +#' \dontrun{ +#' btw_tool_git_status() +#' } +#' +#' @param staged Optional. Return only staged (`TRUE`) or unstaged files +#' (`FALSE`). Use `NULL` to show both (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 Tools +#' @export +btw_tool_git_status <- function(staged, pathspec, `_intent`) {} + +btw_tool_git_status_impl <- function( + staged = NULL, + pathspec = NULL +) { + check_installed("gert") + check_bool(staged, allow_null = TRUE) + check_character(pathspec, allow_null = TRUE) + + status <- gert::git_status(staged = staged, pathspec = pathspec) + + if (nrow(status) == 0) { + return(btw_tool_result("No changes to report")) + } + + # Select relevant columns + fields <- c("file", "status", "staged") + status_display <- status[fields] + + md_res <- md_table(status_display) + + btw_tool_result( + md_res, + data = status_display, + display = list(markdown = md_res) + ) +} + +.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. + +RETURNS: A table showing file paths, their status (new, modified, deleted, etc.), and whether they are staged. + )---", + annotations = ellmer::tool_annotations( + title = "Git Status", + read_only_hint = TRUE, + open_world_hint = FALSE, + idempotent_hint = TRUE, + btw_can_register = function() rlang::is_installed("gert") + ), + arguments = list( + staged = ellmer::type_boolean( + "Optional. Return only staged (TRUE) or unstaged files (FALSE). Use NULL to show both (default).", + 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") +#' } +#' +#' @param ref Optional reference such as `"HEAD"`, a commit SHA, or two refs +#' separated by `..` (e.g., `"main..feature"`). Use `NULL` (default) to diff +#' the working directory against the repository index (unstaged changes). +#' Use `"HEAD"` to see staged changes. +#' @inheritParams btw_tool_docs_package_news +#' +#' @return Returns a diff patch as a formatted string. +#' +#' @family 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 || nzchar(diff_patch) == FALSE) { + 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( + "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 between commits. + +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 = "commit1..commit2"` to compare two commits. +* Use with a branch name to compare current HEAD with that branch. + +RETURNS: A unified diff patch showing the changes. + )---", + 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", a commit SHA, or "ref1..ref2". 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 100. +#' @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 Tools +#' @export +btw_tool_git_log <- function(ref, max, after, `_intent`) {} + +btw_tool_git_log_impl <- function( + ref = "HEAD", + max = 100, + 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 + fields <- c("commit", "author", "time", "message") + log_display <- log[fields] + + # Truncate commit SHA to 7 characters for display + 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) + ) + + md_res <- md_table(log_display) + + btw_tool_result( + md_res, + data = log_display, + display = list(markdown = md_res) + ) +} + +.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 table with commit SHA (short), author, timestamp, and message. + )---", + annotations = ellmer::tool_annotations( + title = "Git Log", + 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( + 'Revision string (branch/tag/commit). Defaults to "HEAD".', + required = FALSE + ), + max = ellmer::type_number( + "Maximum number of commits to retrieve. Defaults to 100.", + 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 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 = HTML("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 after reviewing them with git_status and git_diff. +* 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. +* Review changes with git_status and 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 local Optional. Set `TRUE` to show only local branches, `FALSE` for +#' remote branches, or `NULL` for both. Defaults to `NULL`. +#' @inheritParams btw_tool_docs_package_news +#' +#' @return Returns a character table of branches. +#' +#' @family Tools +#' @export +btw_tool_git_branch_list <- function(local, `_intent`) {} + +btw_tool_git_branch_list_impl <- function(local = NULL) { + check_installed("gert") + check_bool(local, allow_null = TRUE) + + branches <- gert::git_branch_list(local = local) + + if (nrow(branches) == 0) { + return(btw_tool_result("No branches found")) + } + + # Select relevant columns + fields <- c("name", "ref", "upstream", "updated") + # Only include columns that exist + fields <- intersect(fields, names(branches)) + branches_display <- branches[fields] + + md_res <- md_table(branches_display) + + btw_tool_result( + md_res, + data = branches_display, + display = list(markdown = md_res) + ) +} + +.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, refs, 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( + local = ellmer::type_boolean( + "Optional. TRUE for local branches only, FALSE for remote branches only, NULL for both (default).", + 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 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 = md_code_block("", result), + title = HTML("Git Branch Create") + ) + ) +} + +.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 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 git_status first to ensure no uncommitted changes will be lost. +* Use 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 = 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..562b7002 100644 --- a/R/tools.R +++ b/R/tools.R @@ -138,6 +138,7 @@ set_tool_icon <- function(tool, group) { "docs" = tool_icon("dictionary"), "env" = tool_icon("source-environment"), "files" = tool_icon("folder-open"), + "git" = tool_icon("source-environment"), "ide" = tool_icon("code-blocks"), "search" = tool_icon("search"), "session" = tool_icon("screen-search-desktop"), diff --git a/tests/testthat/test-tool-git.R b/tests/testthat/test-tool-git.R new file mode 100644 index 00000000..98a0bf74 --- /dev/null +++ b/tests/testthat/test-tool-git.R @@ -0,0 +1,322 @@ +test_that("btw_tool_git_status() works", { + skip_if_not_installed("gert") + + repo <- withr::local_tempdir() + gert::git_init(repo) + withr::local_dir(repo) + + # Configure user if not set + if (!gert::user_is_configured()) { + gert::git_config_set("user.name", "Test User") + gert::git_config_set("user.email", "test@example.com") + } + + # Empty repo - no changes + result <- btw_tool_git_status() + expect_btw_tool_result(result, has_data = FALSE) + expect_match(result@value, "No changes") + + # Create a new file + writeLines("test content", "test.txt") + + result <- btw_tool_git_status() + expect_btw_tool_result(result) + expect_match(result@value, "test\\.txt") + + # Stage the file + gert::git_add("test.txt") + + # Check staged only + result_staged <- btw_tool_git_status(staged = TRUE) + expect_match(result_staged@value, "test\\.txt") + + # Check unstaged only (should be empty) + result_unstaged <- btw_tool_git_status(staged = FALSE) + expect_btw_tool_result(result_unstaged, has_data = FALSE) + expect_match(result_unstaged@value, "No changes") +}) + +test_that("btw_tool_git_diff() works", { + skip_if_not_installed("gert") + + repo <- withr::local_tempdir() + gert::git_init(repo) + withr::local_dir(repo) + + if (!gert::user_is_configured()) { + gert::git_config_set("user.name", "Test User") + gert::git_config_set("user.email", "test@example.com") + } + + # No changes initially + result <- btw_tool_git_diff() + expect_btw_tool_result(result, has_data = FALSE) + expect_match(result@value, "No.*changes") + + # Create and stage a file + writeLines("line 1", "test.txt") + gert::git_add("test.txt") + gert::git_commit("Initial commit") + + # Modify the file + writeLines(c("line 1", "line 2"), "test.txt") + + # Check unstaged diff (working dir vs index) + result <- btw_tool_git_diff() + expect_btw_tool_result(result, has_data = FALSE) + expect_true(any(grepl("line 2", result@value, fixed = TRUE))) + + # Stage the changes + gert::git_add("test.txt") + + # Check staged diff (index vs HEAD) - shows both lines being modified from HEAD + result_staged <- btw_tool_git_diff(ref = "HEAD") + expect_btw_tool_result(result_staged, has_data = FALSE) + # The staged diff shows the change from HEAD, which includes adding line 2 + expect_true(any(grepl("@@", result_staged@value, fixed = TRUE))) # Has diff hunks + + # Commit and verify no more unstaged changes + gert::git_commit("Add line 2") + result_no_changes <- btw_tool_git_diff() + expect_btw_tool_result(result_no_changes, has_data = FALSE) + expect_match(result_no_changes@value, "No.*changes") +}) + +test_that("btw_tool_git_log() works", { + skip_if_not_installed("gert") + + repo <- withr::local_tempdir() + gert::git_init(repo) + withr::local_dir(repo) + + if (!gert::user_is_configured()) { + gert::git_config_set("user.name", "Test User") + gert::git_config_set("user.email", "test@example.com") + } + + # Create initial commit + writeLines("test", "test.txt") + gert::git_add("test.txt") + gert::git_commit("Initial commit") + + result <- btw_tool_git_log() + expect_btw_tool_result(result) + expect_match(result@value, "Initial commit") + + # Test max parameter + writeLines("test2", "test2.txt") + gert::git_add("test2.txt") + gert::git_commit("Second commit") + + result_limited <- btw_tool_git_log(max = 1) + expect_btw_tool_result(result_limited) + expect_match(result_limited@value, "Second commit") +}) + +test_that("btw_tool_git_commit() works", { + skip_if_not_installed("gert") + + repo <- withr::local_tempdir() + gert::git_init(repo) + withr::local_dir(repo) + + if (!gert::user_is_configured()) { + gert::git_config_set("user.name", "Test User") + gert::git_config_set("user.email", "test@example.com") + } + + # Create a file + writeLines("test content", "test.txt") + + # Commit with files parameter + result <- btw_tool_git_commit( + message = "Add test file", + files = "test.txt" + ) + expect_btw_tool_result(result, has_data = FALSE) + expect_match(result@value, "Add test file") + + # Verify commit was created + log <- gert::git_log(max = 1) + expect_equal(nrow(log), 1) + expect_match(log$message, "Add test file") + + # Test committing already staged files + writeLines("more content", "test2.txt") + gert::git_add("test2.txt") + + result2 <- btw_tool_git_commit( + message = "Add second file", + files = NULL + ) + expect_btw_tool_result(result2, has_data = FALSE) + expect_match(result2@value, "Add second file") +}) + +test_that("btw_tool_git_branch_list() works", { + skip_if_not_installed("gert") + + repo <- withr::local_tempdir() + gert::git_init(repo) + withr::local_dir(repo) + + if (!gert::user_is_configured()) { + gert::git_config_set("user.name", "Test User") + gert::git_config_set("user.email", "test@example.com") + } + + # Create initial commit (needed for branches to show up) + writeLines("test", "test.txt") + gert::git_add("test.txt") + gert::git_commit("Initial commit") + + result <- btw_tool_git_branch_list() + expect_btw_tool_result(result) + # Should show at least the default branch (main or master) + expect_true(nrow(result@extra$data) >= 1) + + # Create a new branch + gert::git_branch_create("feature", checkout = FALSE) + + result2 <- btw_tool_git_branch_list() + expect_btw_tool_result(result2) + expect_true(nrow(result2@extra$data) >= 2) + expect_match(result2@value, "feature") +}) + +test_that("btw_tool_git_branch_create() works", { + skip_if_not_installed("gert") + + repo <- withr::local_tempdir() + gert::git_init(repo) + withr::local_dir(repo) + + if (!gert::user_is_configured()) { + gert::git_config_set("user.name", "Test User") + gert::git_config_set("user.email", "test@example.com") + } + + # Create initial commit + writeLines("test", "test.txt") + gert::git_add("test.txt") + gert::git_commit("Initial commit") + + result <- btw_tool_git_branch_create( + branch = "feature-branch", + checkout = FALSE + ) + expect_btw_tool_result(result, has_data = FALSE) + expect_match(result@value, "feature-branch") + + # Verify branch exists + branches <- gert::git_branch_list() + expect_true("feature-branch" %in% branches$name) +}) + +test_that("btw_tool_git_branch_checkout() works", { + skip_if_not_installed("gert") + + repo <- withr::local_tempdir() + gert::git_init(repo) + withr::local_dir(repo) + + if (!gert::user_is_configured()) { + gert::git_config_set("user.name", "Test User") + gert::git_config_set("user.email", "test@example.com") + } + + # Create initial commit + writeLines("test", "test.txt") + gert::git_add("test.txt") + gert::git_commit("Initial commit") + + # Get current branch name + current_branch <- gert::git_branch() + + # Create and checkout new branch + gert::git_branch_create("feature", checkout = FALSE) + + result <- btw_tool_git_branch_checkout(branch = "feature") + expect_btw_tool_result(result, has_data = FALSE) + expect_match(result@value, "feature") + + # Verify we're on the new branch + expect_equal(gert::git_branch(), "feature") + + # Checkout back to original branch + result2 <- btw_tool_git_branch_checkout(branch = current_branch) + expect_btw_tool_result(result2, has_data = FALSE) + expect_equal(gert::git_branch(), current_branch) +}) + +test_that("git tools require gert to be installed", { + skip_if_not_installed("gert") + skip("Manual test only - requires gert to be uninstalled") + + expect_error( + btw_tool_git_status(), + "gert" + ) + + expect_error( + btw_tool_git_diff(), + "gert" + ) + + expect_error( + btw_tool_git_log(), + "gert" + ) + + expect_error( + btw_tool_git_commit(message = "test", files = "test.txt"), + "gert" + ) + + expect_error( + btw_tool_git_branch_list(), + "gert" + ) + + expect_error( + btw_tool_git_branch_create(branch = "test"), + "gert" + ) + + expect_error( + btw_tool_git_branch_checkout(branch = "main"), + "gert" + ) +}) + +test_that("git tools validate arguments", { + skip_if_not_installed("gert") + + # btw_tool_git_status + expect_error(btw_tool_git_status(staged = "invalid")) + expect_error(btw_tool_git_status(pathspec = 123)) + + # btw_tool_git_diff + expect_error(btw_tool_git_diff(ref = 123)) + + # btw_tool_git_log + expect_error(btw_tool_git_log(ref = 123)) + expect_error(btw_tool_git_log(max = "invalid")) + expect_error(btw_tool_git_log(max = 0)) + + # btw_tool_git_commit + expect_error(btw_tool_git_commit(message = 123)) + expect_error(btw_tool_git_commit(message = "test", files = 123)) + + # btw_tool_git_branch_list + expect_error(btw_tool_git_branch_list(local = "invalid")) + + # btw_tool_git_branch_create + expect_error(btw_tool_git_branch_create(branch = 123)) + expect_error(btw_tool_git_branch_create(branch = "test", ref = 123)) + expect_error(btw_tool_git_branch_create(branch = "test", checkout = "invalid")) + + # btw_tool_git_branch_checkout + expect_error(btw_tool_git_branch_checkout(branch = 123)) + expect_error(btw_tool_git_branch_checkout(branch = "test", force = "invalid")) +}) From 2b1d9f57f83e857dbe3a3db480380cdfc5954b18 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 10:20:35 -0400 Subject: [PATCH 02/23] Refactor git tests to use local_temp_git_repo() helper Creates tests/testthat/helpers-git.R with a helper function that abstracts the common setup pattern for temporary git repositories. This reduces duplication across all git tool tests and makes the tests more maintainable. Generated with Claude Code --- tests/testthat/helpers-git.R | 17 +++++++++ tests/testthat/test-tool-git.R | 64 ++++------------------------------ 2 files changed, 24 insertions(+), 57 deletions(-) create mode 100644 tests/testthat/helpers-git.R diff --git a/tests/testthat/helpers-git.R b/tests/testthat/helpers-git.R new file mode 100644 index 00000000..5eabb452 --- /dev/null +++ b/tests/testthat/helpers-git.R @@ -0,0 +1,17 @@ +local_temp_git_repo <- function( + user_name = "Test User", + user_email = "test@example.com", + env = parent.frame() +) { + repo <- withr::local_tempdir(.local_envir = env) + withr::local_dir(repo, .local_envir = env) + gert::git_init(".") + + # Configure user if not set + if (!gert::user_is_configured()) { + gert::git_config_set("user.name", user_name) + gert::git_config_set("user.email", user_email) + } + + invisible(repo) +} diff --git a/tests/testthat/test-tool-git.R b/tests/testthat/test-tool-git.R index 98a0bf74..3b88e40d 100644 --- a/tests/testthat/test-tool-git.R +++ b/tests/testthat/test-tool-git.R @@ -1,15 +1,7 @@ test_that("btw_tool_git_status() works", { skip_if_not_installed("gert") - repo <- withr::local_tempdir() - gert::git_init(repo) - withr::local_dir(repo) - - # Configure user if not set - if (!gert::user_is_configured()) { - gert::git_config_set("user.name", "Test User") - gert::git_config_set("user.email", "test@example.com") - } + local_temp_git_repo() # Empty repo - no changes result <- btw_tool_git_status() @@ -39,14 +31,7 @@ test_that("btw_tool_git_status() works", { test_that("btw_tool_git_diff() works", { skip_if_not_installed("gert") - repo <- withr::local_tempdir() - gert::git_init(repo) - withr::local_dir(repo) - - if (!gert::user_is_configured()) { - gert::git_config_set("user.name", "Test User") - gert::git_config_set("user.email", "test@example.com") - } + local_temp_git_repo() # No changes initially result <- btw_tool_git_diff() @@ -85,14 +70,7 @@ test_that("btw_tool_git_diff() works", { test_that("btw_tool_git_log() works", { skip_if_not_installed("gert") - repo <- withr::local_tempdir() - gert::git_init(repo) - withr::local_dir(repo) - - if (!gert::user_is_configured()) { - gert::git_config_set("user.name", "Test User") - gert::git_config_set("user.email", "test@example.com") - } + local_temp_git_repo() # Create initial commit writeLines("test", "test.txt") @@ -116,14 +94,7 @@ test_that("btw_tool_git_log() works", { test_that("btw_tool_git_commit() works", { skip_if_not_installed("gert") - repo <- withr::local_tempdir() - gert::git_init(repo) - withr::local_dir(repo) - - if (!gert::user_is_configured()) { - gert::git_config_set("user.name", "Test User") - gert::git_config_set("user.email", "test@example.com") - } + local_temp_git_repo() # Create a file writeLines("test content", "test.txt") @@ -156,14 +127,7 @@ test_that("btw_tool_git_commit() works", { test_that("btw_tool_git_branch_list() works", { skip_if_not_installed("gert") - repo <- withr::local_tempdir() - gert::git_init(repo) - withr::local_dir(repo) - - if (!gert::user_is_configured()) { - gert::git_config_set("user.name", "Test User") - gert::git_config_set("user.email", "test@example.com") - } + local_temp_git_repo() # Create initial commit (needed for branches to show up) writeLines("test", "test.txt") @@ -187,14 +151,7 @@ test_that("btw_tool_git_branch_list() works", { test_that("btw_tool_git_branch_create() works", { skip_if_not_installed("gert") - repo <- withr::local_tempdir() - gert::git_init(repo) - withr::local_dir(repo) - - if (!gert::user_is_configured()) { - gert::git_config_set("user.name", "Test User") - gert::git_config_set("user.email", "test@example.com") - } + local_temp_git_repo() # Create initial commit writeLines("test", "test.txt") @@ -216,14 +173,7 @@ test_that("btw_tool_git_branch_create() works", { test_that("btw_tool_git_branch_checkout() works", { skip_if_not_installed("gert") - repo <- withr::local_tempdir() - gert::git_init(repo) - withr::local_dir(repo) - - if (!gert::user_is_configured()) { - gert::git_config_set("user.name", "Test User") - gert::git_config_set("user.email", "test@example.com") - } + local_temp_git_repo() # Create initial commit writeLines("test", "test.txt") From 4f05d72b2a577e13b3ed8859d7026741333bca84 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 10:34:30 -0400 Subject: [PATCH 03/23] Refactor(tests): Rename helper function argument --- tests/testthat/helpers-git.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/testthat/helpers-git.R b/tests/testthat/helpers-git.R index 5eabb452..498c8f8f 100644 --- a/tests/testthat/helpers-git.R +++ b/tests/testthat/helpers-git.R @@ -1,10 +1,10 @@ local_temp_git_repo <- function( user_name = "Test User", user_email = "test@example.com", - env = parent.frame() + .local_envir = parent.frame() ) { - repo <- withr::local_tempdir(.local_envir = env) - withr::local_dir(repo, .local_envir = env) + repo <- withr::local_tempdir(.local_envir = .local_envir) + withr::local_dir(repo, .local_envir = .local_envir) gert::git_init(".") # Configure user if not set From 18837b66c607b3201d391f72dd2428b326c233c1 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 10:35:19 -0400 Subject: [PATCH 04/23] tests: Check gert not installed errors --- tests/testthat/test-tool-git.R | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/testthat/test-tool-git.R b/tests/testthat/test-tool-git.R index 3b88e40d..f4a8b92e 100644 --- a/tests/testthat/test-tool-git.R +++ b/tests/testthat/test-tool-git.R @@ -58,7 +58,7 @@ test_that("btw_tool_git_diff() works", { result_staged <- btw_tool_git_diff(ref = "HEAD") expect_btw_tool_result(result_staged, has_data = FALSE) # The staged diff shows the change from HEAD, which includes adding line 2 - expect_true(any(grepl("@@", result_staged@value, fixed = TRUE))) # Has diff hunks + expect_true(any(grepl("@@", result_staged@value, fixed = TRUE))) # Has diff hunks # Commit and verify no more unstaged changes gert::git_commit("Add line 2") @@ -201,7 +201,12 @@ test_that("btw_tool_git_branch_checkout() works", { test_that("git tools require gert to be installed", { skip_if_not_installed("gert") - skip("Manual test only - requires gert to be uninstalled") + + local_temp_git_repo() + + local_mocked_bindings( + is_installed = function(pkg) pkg != "gert" + ) expect_error( btw_tool_git_status(), @@ -241,6 +246,7 @@ test_that("git tools require gert to be installed", { test_that("git tools validate arguments", { skip_if_not_installed("gert") + local_temp_git_repo() # btw_tool_git_status expect_error(btw_tool_git_status(staged = "invalid")) @@ -264,7 +270,9 @@ test_that("git tools validate arguments", { # btw_tool_git_branch_create expect_error(btw_tool_git_branch_create(branch = 123)) expect_error(btw_tool_git_branch_create(branch = "test", ref = 123)) - expect_error(btw_tool_git_branch_create(branch = "test", checkout = "invalid")) + expect_error( + btw_tool_git_branch_create(branch = "test", checkout = "invalid") + ) # btw_tool_git_branch_checkout expect_error(btw_tool_git_branch_checkout(branch = 123)) From 55ea2533868342e71abe3fc3e62374c4ab4f9206 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 10:35:39 -0400 Subject: [PATCH 05/23] tests: Remove " works" --- tests/testthat/test-tool-git.R | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/testthat/test-tool-git.R b/tests/testthat/test-tool-git.R index f4a8b92e..67b1fdf4 100644 --- a/tests/testthat/test-tool-git.R +++ b/tests/testthat/test-tool-git.R @@ -1,4 +1,4 @@ -test_that("btw_tool_git_status() works", { +test_that("btw_tool_git_status()", { skip_if_not_installed("gert") local_temp_git_repo() @@ -28,7 +28,7 @@ test_that("btw_tool_git_status() works", { expect_match(result_unstaged@value, "No changes") }) -test_that("btw_tool_git_diff() works", { +test_that("btw_tool_git_diff()", { skip_if_not_installed("gert") local_temp_git_repo() @@ -67,7 +67,7 @@ test_that("btw_tool_git_diff() works", { expect_match(result_no_changes@value, "No.*changes") }) -test_that("btw_tool_git_log() works", { +test_that("btw_tool_git_log()", { skip_if_not_installed("gert") local_temp_git_repo() @@ -91,7 +91,7 @@ test_that("btw_tool_git_log() works", { expect_match(result_limited@value, "Second commit") }) -test_that("btw_tool_git_commit() works", { +test_that("btw_tool_git_commit()", { skip_if_not_installed("gert") local_temp_git_repo() @@ -124,7 +124,7 @@ test_that("btw_tool_git_commit() works", { expect_match(result2@value, "Add second file") }) -test_that("btw_tool_git_branch_list() works", { +test_that("btw_tool_git_branch_list()", { skip_if_not_installed("gert") local_temp_git_repo() @@ -148,7 +148,7 @@ test_that("btw_tool_git_branch_list() works", { expect_match(result2@value, "feature") }) -test_that("btw_tool_git_branch_create() works", { +test_that("btw_tool_git_branch_create()", { skip_if_not_installed("gert") local_temp_git_repo() @@ -170,7 +170,7 @@ test_that("btw_tool_git_branch_create() works", { expect_true("feature-branch" %in% branches$name) }) -test_that("btw_tool_git_branch_checkout() works", { +test_that("btw_tool_git_branch_checkout()", { skip_if_not_installed("gert") local_temp_git_repo() From 752293536989cc1c14f6d5f2b6e1c851f502ef21 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 11:17:16 -0400 Subject: [PATCH 06/23] Add snapshot tests for git tool outputs Adds snapshots to capture the key outputs that are returned to LLMs for each git tool. These snapshots provide a clear picture of the data format and content being sent back, making it easier to validate that tools are returning appropriate information. Snapshots cover: - git_status: file status tables and empty state messages - git_diff: diff patches and no-changes messages - git_log: commit history tables - git_commit: commit confirmation messages - git_branch_list: branch listing tables - git_branch_create: branch creation confirmations - git_branch_checkout: checkout confirmations Generated with Claude Code --- tests/testthat/_snaps/tool-git.md | 88 +++++++++++++++++++++++++++++++ tests/testthat/test-tool-git.R | 15 ++++++ 2 files changed, 103 insertions(+) create mode 100644 tests/testthat/_snaps/tool-git.md diff --git a/tests/testthat/_snaps/tool-git.md b/tests/testthat/_snaps/tool-git.md new file mode 100644 index 00000000..09d6f68c --- /dev/null +++ b/tests/testthat/_snaps/tool-git.md @@ -0,0 +1,88 @@ +# btw_tool_git_status() + + Code + cli::cat_line(result@value) + Output + | file | status | staged | + |------|--------|--------| + | test.txt | new | FALSE | + +--- + + Code + cli::cat_line(result_staged_empty@value) + Output + No changes to report + +--- + + Code + cli::cat_line(result_staged@value) + Output + | file | status | staged | + |------|--------|--------| + | test.txt | new | TRUE | + +# btw_tool_git_diff() + + Code + cli::cat_line(result@value) + Output + No unstaged changes to show + +--- + + Code + cli::cat_line(result@value) + Output + ```diff + diff --git a/test.txt b/test.txt + index 89b24ec..7bba8c8 100644 + --- a/test.txt + +++ b/test.txt + @@ -1 +1,2 @@ + line 1 + +line 2 + + ``` + +# btw_tool_git_log() + + Code + cli::cat_line(result@value) + Output + | commit | author | time | message | + |--------|--------|------|---------| + | 5da1156 | Garrick Aden-Buie | 2025-10-15 10:41:23 | Initial commit | + +# btw_tool_git_commit() + + Code + cli::cat_line(result@value) + Output + Created commit: 56fa58a + Message: Add test file + +# btw_tool_git_branch_list() + + Code + cli::cat_line(result@value) + Output + | name | ref | upstream | updated | + |------|-----|----------|---------| + | main | refs/heads/main | NA | 2025-10-15 10:41:23 | + +# btw_tool_git_branch_create() + + Code + cli::cat_line(result@value) + Output + Created branch 'feature-branch' from 'HEAD' + +# btw_tool_git_branch_checkout() + + Code + cli::cat_line(result@value) + Output + Checked out branch 'feature' + diff --git a/tests/testthat/test-tool-git.R b/tests/testthat/test-tool-git.R index 67b1fdf4..6b2f022e 100644 --- a/tests/testthat/test-tool-git.R +++ b/tests/testthat/test-tool-git.R @@ -14,6 +14,13 @@ test_that("btw_tool_git_status()", { result <- btw_tool_git_status() expect_btw_tool_result(result) expect_match(result@value, "test\\.txt") + expect_snapshot(cli::cat_line(result@value)) + + # Check staged only (should be empty) + result_staged_empty <- btw_tool_git_status(staged = TRUE) + expect_btw_tool_result(result_staged_empty, has_data = FALSE) + expect_match(result_staged_empty@value, "No changes") + expect_snapshot(cli::cat_line(result_staged_empty@value)) # Stage the file gert::git_add("test.txt") @@ -21,6 +28,7 @@ test_that("btw_tool_git_status()", { # Check staged only result_staged <- btw_tool_git_status(staged = TRUE) expect_match(result_staged@value, "test\\.txt") + expect_snapshot(cli::cat_line(result_staged@value)) # Check unstaged only (should be empty) result_unstaged <- btw_tool_git_status(staged = FALSE) @@ -37,6 +45,7 @@ test_that("btw_tool_git_diff()", { result <- btw_tool_git_diff() expect_btw_tool_result(result, has_data = FALSE) expect_match(result@value, "No.*changes") + expect_snapshot(cli::cat_line(result@value)) # Create and stage a file writeLines("line 1", "test.txt") @@ -50,6 +59,7 @@ test_that("btw_tool_git_diff()", { result <- btw_tool_git_diff() expect_btw_tool_result(result, has_data = FALSE) expect_true(any(grepl("line 2", result@value, fixed = TRUE))) + expect_snapshot(cli::cat_line(result@value)) # Stage the changes gert::git_add("test.txt") @@ -80,6 +90,7 @@ test_that("btw_tool_git_log()", { result <- btw_tool_git_log() expect_btw_tool_result(result) expect_match(result@value, "Initial commit") + expect_snapshot(cli::cat_line(result@value)) # Test max parameter writeLines("test2", "test2.txt") @@ -106,6 +117,7 @@ test_that("btw_tool_git_commit()", { ) expect_btw_tool_result(result, has_data = FALSE) expect_match(result@value, "Add test file") + expect_snapshot(cli::cat_line(result@value)) # Verify commit was created log <- gert::git_log(max = 1) @@ -138,6 +150,7 @@ test_that("btw_tool_git_branch_list()", { expect_btw_tool_result(result) # Should show at least the default branch (main or master) expect_true(nrow(result@extra$data) >= 1) + expect_snapshot(cli::cat_line(result@value)) # Create a new branch gert::git_branch_create("feature", checkout = FALSE) @@ -164,6 +177,7 @@ test_that("btw_tool_git_branch_create()", { ) expect_btw_tool_result(result, has_data = FALSE) expect_match(result@value, "feature-branch") + expect_snapshot(cli::cat_line(result@value)) # Verify branch exists branches <- gert::git_branch_list() @@ -189,6 +203,7 @@ test_that("btw_tool_git_branch_checkout()", { result <- btw_tool_git_branch_checkout(branch = "feature") expect_btw_tool_result(result, has_data = FALSE) expect_match(result@value, "feature") + expect_snapshot(cli::cat_line(result@value)) # Verify we're on the new branch expect_equal(gert::git_branch(), "feature") From d4bae00e5560c98a93d1b113dc790a33155ce6cb Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 12:00:46 -0400 Subject: [PATCH 07/23] Add scrub_git_details() to stabilize git test snapshots Creates a helper function in tests/testthat/helpers-git.R that replaces variable git details (timestamps and commit SHAs) with fixed placeholder values. This ensures snapshot tests don't change on every test run. The function: - Replaces timestamps (YYYY-MM-DD HH:MM:SS) with "2025-10-11 12:13:14" - Replaces 7-character commit SHAs with "abcd123" All snapshot tests now use `transform = scrub_git_details` to ensure stable, reproducible snapshots across test runs. Generated with Claude Code --- tests/testthat/_snaps/tool-git.md | 15 ++++----------- tests/testthat/helpers-git.R | 25 ++++++++++++++++++++----- tests/testthat/test-tool-git.R | 19 +++++++++---------- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/tests/testthat/_snaps/tool-git.md b/tests/testthat/_snaps/tool-git.md index 09d6f68c..a50e00b1 100644 --- a/tests/testthat/_snaps/tool-git.md +++ b/tests/testthat/_snaps/tool-git.md @@ -25,19 +25,12 @@ # btw_tool_git_diff() - Code - cli::cat_line(result@value) - Output - No unstaged changes to show - ---- - Code cli::cat_line(result@value) Output ```diff diff --git a/test.txt b/test.txt - index 89b24ec..7bba8c8 100644 + index abcd123..abcd123 100644 --- a/test.txt +++ b/test.txt @@ -1 +1,2 @@ @@ -53,14 +46,14 @@ Output | commit | author | time | message | |--------|--------|------|---------| - | 5da1156 | Garrick Aden-Buie | 2025-10-15 10:41:23 | Initial commit | + | abcd123 | Test User | 2025-10-11 12:13:14 | Initial commit | # btw_tool_git_commit() Code cli::cat_line(result@value) Output - Created commit: 56fa58a + Created commit: abcd123 Message: Add test file # btw_tool_git_branch_list() @@ -70,7 +63,7 @@ Output | name | ref | upstream | updated | |------|-----|----------|---------| - | main | refs/heads/main | NA | 2025-10-15 10:41:23 | + | main | refs/heads/main | NA | 2025-10-11 12:13:14 | # btw_tool_git_branch_create() diff --git a/tests/testthat/helpers-git.R b/tests/testthat/helpers-git.R index 498c8f8f..dc36bee6 100644 --- a/tests/testthat/helpers-git.R +++ b/tests/testthat/helpers-git.R @@ -7,11 +7,26 @@ local_temp_git_repo <- function( withr::local_dir(repo, .local_envir = .local_envir) gert::git_init(".") - # Configure user if not set - if (!gert::user_is_configured()) { - gert::git_config_set("user.name", user_name) - gert::git_config_set("user.email", user_email) - } + gert::git_config_set("user.name", user_name) + gert::git_config_set("user.email", user_email) invisible(repo) } + +scrub_git_details <- function(x) { + # Replace timestamps (YYYY-MM-DD HH:MM:SS format) + x <- gsub( + "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}", + "2025-10-11 12:13:14", + x + ) + + # Replace 7-character commit SHAs + x <- gsub( + "\\b[0-9a-f]{7}\\b", + "abcd123", + x + ) + + x +} diff --git a/tests/testthat/test-tool-git.R b/tests/testthat/test-tool-git.R index 6b2f022e..8f02a1a0 100644 --- a/tests/testthat/test-tool-git.R +++ b/tests/testthat/test-tool-git.R @@ -14,13 +14,13 @@ test_that("btw_tool_git_status()", { result <- btw_tool_git_status() expect_btw_tool_result(result) expect_match(result@value, "test\\.txt") - expect_snapshot(cli::cat_line(result@value)) + expect_snapshot(cli::cat_line(result@value), transform = scrub_git_details) # Check staged only (should be empty) result_staged_empty <- btw_tool_git_status(staged = TRUE) expect_btw_tool_result(result_staged_empty, has_data = FALSE) expect_match(result_staged_empty@value, "No changes") - expect_snapshot(cli::cat_line(result_staged_empty@value)) + expect_snapshot(cli::cat_line(result_staged_empty@value), transform = scrub_git_details) # Stage the file gert::git_add("test.txt") @@ -28,7 +28,7 @@ test_that("btw_tool_git_status()", { # Check staged only result_staged <- btw_tool_git_status(staged = TRUE) expect_match(result_staged@value, "test\\.txt") - expect_snapshot(cli::cat_line(result_staged@value)) + expect_snapshot(cli::cat_line(result_staged@value), transform = scrub_git_details) # Check unstaged only (should be empty) result_unstaged <- btw_tool_git_status(staged = FALSE) @@ -45,7 +45,6 @@ test_that("btw_tool_git_diff()", { result <- btw_tool_git_diff() expect_btw_tool_result(result, has_data = FALSE) expect_match(result@value, "No.*changes") - expect_snapshot(cli::cat_line(result@value)) # Create and stage a file writeLines("line 1", "test.txt") @@ -59,7 +58,7 @@ test_that("btw_tool_git_diff()", { result <- btw_tool_git_diff() expect_btw_tool_result(result, has_data = FALSE) expect_true(any(grepl("line 2", result@value, fixed = TRUE))) - expect_snapshot(cli::cat_line(result@value)) + expect_snapshot(cli::cat_line(result@value), transform = scrub_git_details) # Stage the changes gert::git_add("test.txt") @@ -90,7 +89,7 @@ test_that("btw_tool_git_log()", { result <- btw_tool_git_log() expect_btw_tool_result(result) expect_match(result@value, "Initial commit") - expect_snapshot(cli::cat_line(result@value)) + expect_snapshot(cli::cat_line(result@value), transform = scrub_git_details) # Test max parameter writeLines("test2", "test2.txt") @@ -117,7 +116,7 @@ test_that("btw_tool_git_commit()", { ) expect_btw_tool_result(result, has_data = FALSE) expect_match(result@value, "Add test file") - expect_snapshot(cli::cat_line(result@value)) + expect_snapshot(cli::cat_line(result@value), transform = scrub_git_details) # Verify commit was created log <- gert::git_log(max = 1) @@ -150,7 +149,7 @@ test_that("btw_tool_git_branch_list()", { expect_btw_tool_result(result) # Should show at least the default branch (main or master) expect_true(nrow(result@extra$data) >= 1) - expect_snapshot(cli::cat_line(result@value)) + expect_snapshot(cli::cat_line(result@value), transform = scrub_git_details) # Create a new branch gert::git_branch_create("feature", checkout = FALSE) @@ -177,7 +176,7 @@ test_that("btw_tool_git_branch_create()", { ) expect_btw_tool_result(result, has_data = FALSE) expect_match(result@value, "feature-branch") - expect_snapshot(cli::cat_line(result@value)) + expect_snapshot(cli::cat_line(result@value), transform = scrub_git_details) # Verify branch exists branches <- gert::git_branch_list() @@ -203,7 +202,7 @@ test_that("btw_tool_git_branch_checkout()", { result <- btw_tool_git_branch_checkout(branch = "feature") expect_btw_tool_result(result, has_data = FALSE) expect_match(result@value, "feature") - expect_snapshot(cli::cat_line(result@value)) + expect_snapshot(cli::cat_line(result@value), transform = scrub_git_details) # Verify we're on the new branch expect_equal(gert::git_branch(), "feature") From dd1eeb6086b233656202c38f46f67d7f5393cad9 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 12:15:29 -0400 Subject: [PATCH 08/23] tests: Add test for integrated workflow --- tests/testthat/_snaps/tool-git.md | 62 ++++++++++++++++ tests/testthat/test-tool-git.R | 118 +++++++++++++++++++++++++++++- 2 files changed, 178 insertions(+), 2 deletions(-) diff --git a/tests/testthat/_snaps/tool-git.md b/tests/testthat/_snaps/tool-git.md index a50e00b1..c15adead 100644 --- a/tests/testthat/_snaps/tool-git.md +++ b/tests/testthat/_snaps/tool-git.md @@ -79,3 +79,65 @@ Output Checked out branch 'feature' +# git tools work together + + Code + cli::cat_line(diff_unstaged@value) + Output + ```diff + diff --git a/file1.txt b/file1.txt + index abcd123..abcd123 100644 + --- a/file1.txt + +++ b/file1.txt + @@ -1 +1,2 @@ + Initial content + +Added line + + ``` + +--- + + Code + cli::cat_line(diff_staged@value) + Output + ```diff + diff --git a/file1.txt b/file1.txt + new file mode 100644 + index abcd123..abcd123 + --- /dev/null + +++ b/file1.txt + @@ -0,0 +1 @@ + +Initial content + + ``` + +--- + + Code + cli::cat_line(log_all@value) + Output + | commit | author | time | message | + |--------|--------|------|---------| + | abcd123 | Test User | 2025-10-11 12:13:14 | Update file1 | + | abcd123 | Test User | 2025-10-11 12:13:14 | Add file1 | + +--- + + Code + cli::cat_line(status_final@value) + Output + | file | status | staged | + |------|--------|--------| + | file3.txt | new | FALSE | + +--- + + Code + cli::cat_line(log_final@value) + Output + | commit | author | time | message | + |--------|--------|------|---------| + | abcd123 | Test User | 2025-10-11 12:13:14 | Add file2 | + | abcd123 | Test User | 2025-10-11 12:13:14 | Update file1 | + | abcd123 | Test User | 2025-10-11 12:13:14 | Add file1 | + diff --git a/tests/testthat/test-tool-git.R b/tests/testthat/test-tool-git.R index 8f02a1a0..620ae9c7 100644 --- a/tests/testthat/test-tool-git.R +++ b/tests/testthat/test-tool-git.R @@ -20,7 +20,10 @@ test_that("btw_tool_git_status()", { result_staged_empty <- btw_tool_git_status(staged = TRUE) expect_btw_tool_result(result_staged_empty, has_data = FALSE) expect_match(result_staged_empty@value, "No changes") - expect_snapshot(cli::cat_line(result_staged_empty@value), transform = scrub_git_details) + expect_snapshot( + cli::cat_line(result_staged_empty@value), + transform = scrub_git_details + ) # Stage the file gert::git_add("test.txt") @@ -28,7 +31,10 @@ test_that("btw_tool_git_status()", { # Check staged only result_staged <- btw_tool_git_status(staged = TRUE) expect_match(result_staged@value, "test\\.txt") - expect_snapshot(cli::cat_line(result_staged@value), transform = scrub_git_details) + expect_snapshot( + cli::cat_line(result_staged@value), + transform = scrub_git_details + ) # Check unstaged only (should be empty) result_unstaged <- btw_tool_git_status(staged = FALSE) @@ -292,3 +298,111 @@ test_that("git tools validate arguments", { expect_error(btw_tool_git_branch_checkout(branch = 123)) expect_error(btw_tool_git_branch_checkout(branch = "test", force = "invalid")) }) + +test_that("git tools work together", { + skip_if_not_installed("gert") + local_temp_git_repo() + + # Workflow 1: Check status, stage, commit, verify log + # Create some files + writeLines("Initial content", "file1.txt") + writeLines("More content", "file2.txt") + + # Check status to see untracked files + status1 <- btw_tool_git_status() + expect_match(status1@value, "file1\\.txt") + expect_match(status1@value, "file2\\.txt") + + # Stage and commit first file + commit1 <- btw_tool_git_commit( + message = "Add file1", + files = "file1.txt" + ) + expect_btw_tool_result(commit1, has_data = FALSE) + + # Verify log shows the commit + log1 <- btw_tool_git_log(max = 1) + expect_match(log1@value, "Add file1") + + # Extract commit SHA from log data to use in diff + commit_sha <- log1@extra$data$commit[1] + expect_true(nchar(commit_sha) == 7) + + # Workflow 2: Make changes, check diff, stage, commit + # Modify file1 + writeLines(c("Initial content", "Added line"), "file1.txt") + + # Check diff shows unstaged changes + diff_unstaged <- btw_tool_git_diff() + expect_btw_tool_result(diff_unstaged, has_data = FALSE) + # Should show a diff with changes + expect_true(any(grepl("@@", diff_unstaged@value, fixed = TRUE))) + expect_snapshot( + cli::cat_line(diff_unstaged@value), + transform = scrub_git_details + ) + + # Stage the changes + gert::git_add("file1.txt") + + # Check diff shows staged changes + diff_staged <- btw_tool_git_diff(ref = "HEAD") + expect_btw_tool_result(diff_staged, has_data = FALSE) + # Should show a diff with changes + expect_true(any(grepl("@@", diff_staged@value, fixed = TRUE))) + expect_snapshot( + cli::cat_line(diff_staged@value), + transform = scrub_git_details + ) + + # Commit the changes + commit2 <- btw_tool_git_commit( + message = "Update file1", + files = NULL # Already staged + ) + expect_btw_tool_result(commit2, has_data = FALSE) + + # Workflow 3: Use log to find commits + # Get log of both commits + log_all <- btw_tool_git_log(max = 10) + expect_btw_tool_result(log_all) + expect_equal(nrow(log_all@extra$data), 2) + expect_match(log_all@value, "Add file1") + expect_match(log_all@value, "Update file1") + expect_snapshot(cli::cat_line(log_all@value), transform = scrub_git_details) + + # Get commits SHAs from log - verifies we can use log data + commit_shas <- log_all@extra$data$commit + expect_equal(length(commit_shas), 2) + # Both should be 7-character SHAs + expect_true(all(nchar(commit_shas) == 7)) + + # Workflow 4: Multiple file workflow with status tracking + # Add second file + commit3 <- btw_tool_git_commit( + message = "Add file2", + files = "file2.txt" + ) + expect_btw_tool_result(commit3, has_data = FALSE) + + # Create third file but don't commit + writeLines("Third file", "file3.txt") + + # Status should show file3 as untracked, others committed + status_final <- btw_tool_git_status() + expect_match(status_final@value, "file3\\.txt") + expect_no_match(status_final@value, "file1\\.txt") + expect_no_match(status_final@value, "file2\\.txt") + expect_snapshot( + cli::cat_line(status_final@value), + transform = scrub_git_details + ) + + # Verify log shows all three commits + log_final <- btw_tool_git_log(max = 10) + expect_equal(nrow(log_final@extra$data), 3) + expect_match(log_final@value, "Add file1") + expect_match(log_final@value, "Update file1") + expect_match(log_final@value, "Add file2") + expect_snapshot(cli::cat_line(log_final@value), transform = scrub_git_details) +}) From eb6a85c1e37f05dc7f648ec31f6933970eca4d53 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 14:03:00 -0400 Subject: [PATCH 09/23] feat: Rework outputs from git tools, add `md_kv_table()` helper --- NAMESPACE | 7 ++ R/tool-git.R | 131 +++++++++++++++------------- R/utils-md.R | 21 +++++ man/btw_tool_git_branch_checkout.Rd | 40 +++++++++ man/btw_tool_git_branch_create.Rd | 47 ++++++++++ man/btw_tool_git_branch_list.Rd | 39 +++++++++ man/btw_tool_git_commit.Rd | 40 +++++++++ man/btw_tool_git_diff.Rd | 41 +++++++++ man/btw_tool_git_log.Rd | 44 ++++++++++ man/btw_tool_git_status.Rd | 40 +++++++++ man/btw_tools.Rd | 7 ++ tests/testthat/_snaps/tool-git.md | 91 +++++++++++++------ tests/testthat/test-tool-git.R | 28 +++++- 13 files changed, 488 insertions(+), 88 deletions(-) create mode 100644 man/btw_tool_git_branch_checkout.Rd create mode 100644 man/btw_tool_git_branch_create.Rd create mode 100644 man/btw_tool_git_branch_list.Rd create mode 100644 man/btw_tool_git_commit.Rd create mode 100644 man/btw_tool_git_diff.Rd create mode 100644 man/btw_tool_git_log.Rd create mode 100644 man/btw_tool_git_status.Rd 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/tool-git.R b/R/tool-git.R index 4cbdddd6..a7cb4d5f 100644 --- a/R/tool-git.R +++ b/R/tool-git.R @@ -17,7 +17,7 @@ NULL #' #' @return Returns a character table of file statuses. #' -#' @family Tools +#' @family git tools #' @export btw_tool_git_status <- function(staged, pathspec, `_intent`) {} @@ -35,16 +35,14 @@ btw_tool_git_status_impl <- function( return(btw_tool_result("No changes to report")) } - # Select relevant columns - fields <- c("file", "status", "staged") - status_display <- status[fields] - - md_res <- md_table(status_display) + md_res <- glue_( + "{{ status$file }} [{{ status$status }}]{{ ifelse(status$staged, ' +staged', ' -unstaged') }}" + ) btw_tool_result( - md_res, - data = status_display, - display = list(markdown = md_res) + paste(md_res, collapse = "\n"), + data = status, + display = list(markdown = paste("*", md_res, collapse = "\n")) ) } @@ -60,19 +58,20 @@ btw_tool_git_status_impl <- function( 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 table showing file paths, their status (new, modified, deleted, etc.), and whether they are staged. +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 = TRUE, + idempotent_hint = FALSE, btw_can_register = function() rlang::is_installed("gert") ), arguments = list( staged = ellmer::type_boolean( - "Optional. Return only staged (TRUE) or unstaged files (FALSE). Use NULL to show both (default).", + "Optional. Return only staged (true) or unstaged files (false). Use null to show both (default).", required = FALSE ), pathspec = ellmer::type_array( @@ -103,7 +102,7 @@ RETURNS: A table showing file paths, their status (new, modified, deleted, etc.) #' #' @return Returns a diff patch as a formatted string. #' -#' @family Tools +#' @family git tools #' @export btw_tool_git_diff <- function(ref, `_intent`) {} @@ -130,7 +129,7 @@ btw_tool_git_diff_impl <- function(ref = NULL) { display = list( markdown = value, title = HTML(sprintf( - "Diff%s", + "Git Diff%s", if (!is.null(ref)) sprintf(" (%s)", ref) else "" )) ) @@ -183,20 +182,20 @@ RETURNS: A unified diff patch showing the changes. #' #' @param ref Revision string with a branch/tag/commit value. Defaults to #' `"HEAD"`. -#' @param max Maximum number of commits to retrieve. Defaults to 100. +#' @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 Tools +#' @family git tools #' @export btw_tool_git_log <- function(ref, max, after, `_intent`) {} btw_tool_git_log_impl <- function( ref = "HEAD", - max = 100, + max = 10, after = NULL ) { check_installed("gert") @@ -211,10 +210,11 @@ btw_tool_git_log_impl <- function( } # Select and format relevant columns - fields <- c("commit", "author", "time", "message") - log_display <- log[fields] + 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 @@ -231,12 +231,12 @@ btw_tool_git_log_impl <- function( character(1) ) - md_res <- md_table(log_display) - btw_tool_result( - md_res, - data = log_display, - display = list(markdown = md_res) + md_kv_table(log_display[fields]), + data = log, + display = list( + markdown = md_table(log_display[rev(fields)]) + ) ) } @@ -254,13 +254,13 @@ WHEN TO USE: * Useful for understanding the history of changes before making new commits. * Can filter by branch, number of commits, or date range. -RETURNS: A table with commit SHA (short), author, timestamp, and message. +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 = TRUE, + idempotent_hint = FALSE, btw_can_register = function() rlang::is_installed("gert") ), arguments = list( @@ -269,7 +269,7 @@ RETURNS: A table with commit SHA (short), author, timestamp, and message. required = FALSE ), max = ellmer::type_number( - "Maximum number of commits to retrieve. Defaults to 100.", + "Maximum number of commits to retrieve. Defaults to 10.", required = FALSE ), after = ellmer::type_string( @@ -297,7 +297,7 @@ RETURNS: A table with commit SHA (short), author, timestamp, and message. #' #' @return Returns the commit SHA. #' -#' @family Tools +#' @family git tools #' @export btw_tool_git_commit <- function(message, files, `_intent`) {} @@ -328,7 +328,7 @@ btw_tool_git_commit_impl <- function( data = list(sha = commit_sha, message = message), display = list( markdown = md_code_block("", result), - title = HTML("Git Commit") + title = "Git Commit" ) ) } @@ -343,13 +343,13 @@ btw_tool_git_commit_impl <- function( description = r"---(Stage files and create a git commit. WHEN TO USE: -* Use this tool to commit changes after reviewing them with git_status and git_diff. +* 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. -* Review changes with git_status and git_diff before committing. +* 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. @@ -385,19 +385,23 @@ RETURNS: The commit SHA and confirmation message. #' btw_tool_git_branch_list(local = FALSE) # remote branches #' } #' -#' @param local Optional. Set `TRUE` to show only local branches, `FALSE` for -#' remote branches, or `NULL` for both. Defaults to `NULL`. +#' @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 Tools +#' @family git tools #' @export -btw_tool_git_branch_list <- function(local, `_intent`) {} +btw_tool_git_branch_list <- function(include, `_intent`) {} -btw_tool_git_branch_list_impl <- function(local = NULL) { +btw_tool_git_branch_list_impl <- function( + include = c("local", "remote", "all") +) { check_installed("gert") - check_bool(local, allow_null = TRUE) + include <- arg_match(include) + + local <- switch(include, local = TRUE, remote = FALSE, all = NULL) branches <- gert::git_branch_list(local = local) @@ -406,17 +410,18 @@ btw_tool_git_branch_list_impl <- function(local = NULL) { } # Select relevant columns - fields <- c("name", "ref", "upstream", "updated") - # Only include columns that exist - fields <- intersect(fields, names(branches)) - branches_display <- branches[fields] + fields <- c("name", "upstream", "updated") - md_res <- md_table(branches_display) + 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( - md_res, - data = branches_display, - display = list(markdown = md_res) + paste(branches_llm, collapse = "\n"), + data = branches, + display = list(markdown = md_table(branches[fields])) ) } @@ -433,7 +438,7 @@ 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, refs, upstream tracking, and last update time. +RETURNS: A table of branch names, upstream tracking, and last update time. )---", annotations = ellmer::tool_annotations( title = "Git Branches", @@ -443,8 +448,9 @@ RETURNS: A table of branch names, refs, upstream tracking, and last update time. btw_can_register = function() rlang::is_installed("gert") ), arguments = list( - local = ellmer::type_boolean( - "Optional. TRUE for local branches only, FALSE for remote branches only, NULL for both (default).", + include = ellmer::type_enum( + c("local", "remote", "all"), + 'Optional. Filter branches to "local" (default), "remote", or "all".', required = FALSE ) ) @@ -463,12 +469,13 @@ RETURNS: A table of branch names, refs, upstream tracking, and last update time. #' #' @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`. +#' @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 Tools +#' @family git tools #' @export btw_tool_git_branch_create <- function(branch, ref, checkout, `_intent`) {} @@ -485,7 +492,7 @@ btw_tool_git_branch_create_impl <- function( gert::git_branch_create(branch = branch, ref = ref, checkout = checkout) result <- sprintf( - "Created branch '%s' from '%s'%s", + "Created branch `%s` from `%s`%s.", branch, ref, if (checkout) " and checked it out" else "" @@ -494,8 +501,11 @@ btw_tool_git_branch_create_impl <- function( btw_tool_result( result, display = list( - markdown = md_code_block("", result), - title = HTML("Git Branch Create") + markdown = result, + title = htmltools::HTML(sprintf( + "Git Create Branch %s", + branch + )) ) ) } @@ -554,12 +564,13 @@ RETURNS: Confirmation message with branch name and ref. #' } #' #' @param branch Name of branch to check out. -#' @param force Whether to force checkout even with uncommitted changes. Defaults to `FALSE`. +#' @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 Tools +#' @family git tools #' @export btw_tool_git_branch_checkout <- function(branch, force, `_intent`) {} @@ -595,13 +606,13 @@ btw_tool_git_branch_checkout_impl <- function( WHEN TO USE: * Use this tool to switch between existing branches. -* Check git_status first to ensure no uncommitted changes will be lost. -* Use git_branch_list to see available 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 = TRUE. -* Using force = TRUE can lose uncommitted changes. +* Will fail if there are uncommitted changes unless force is true. +* Using `force: true` can lose uncommitted changes. RETURNS: Confirmation message with branch name. )---", @@ -617,7 +628,7 @@ RETURNS: Confirmation message with branch name. "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.", + "Whether to force checkout even with uncommitted changes. Defaults to false. Use with caution.", required = FALSE ) ) 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/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..a5f339e8 --- /dev/null +++ b/man/btw_tool_git_diff.Rd @@ -0,0 +1,41 @@ +% 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}{Optional reference such as \code{"HEAD"}, a commit SHA, or two refs +separated by \code{..} (e.g., \code{"main..feature"}). Use \code{NULL} (default) to diff +the working directory against the repository index (unstaged changes). +Use \code{"HEAD"} to see staged changes.} + +\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..fa11f852 --- /dev/null +++ b/man/btw_tool_git_status.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_status} +\alias{btw_tool_git_status} +\title{Tool: Git Status} +\usage{ +btw_tool_git_status(staged = NULL, pathspec = NULL, `_intent` = "") +} +\arguments{ +\item{staged}{Optional. Return only staged (\code{TRUE}) or unstaged files +(\code{FALSE}). Use \code{NULL} to show both (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..bd4135d2 100644 --- a/man/btw_tools.Rd +++ b/man/btw_tools.Rd @@ -42,6 +42,13 @@ this function have access to the tools:\tabular{lll}{ 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_git_branch_checkout \tab git \tab Switch to a different git branch. \cr + btw_tool_git_branch_create \tab git \tab Create a new git branch. \cr + btw_tool_git_branch_list \tab git \tab List git branches in the repository. \cr + btw_tool_git_commit \tab git \tab Stage files and create a git commit. \cr + btw_tool_git_diff \tab git \tab View changes in the working directory or between commits. \cr + btw_tool_git_log \tab git \tab Show the commit history for a repository. \cr + btw_tool_git_status \tab git \tab Show the status of the git working directory. \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 diff --git a/tests/testthat/_snaps/tool-git.md b/tests/testthat/_snaps/tool-git.md index c15adead..435e1e0c 100644 --- a/tests/testthat/_snaps/tool-git.md +++ b/tests/testthat/_snaps/tool-git.md @@ -3,9 +3,7 @@ Code cli::cat_line(result@value) Output - | file | status | staged | - |------|--------|--------| - | test.txt | new | FALSE | + test.txt [new] -unstaged --- @@ -19,9 +17,35 @@ Code cli::cat_line(result_staged@value) Output - | file | status | staged | - |------|--------|--------| - | test.txt | new | TRUE | + test.txt [new] +staged + +--- + + Code + cli::cat_line(btw_tool_git_status()@value) + Output + test.txt [new] +staged + +--- + + Code + cli::cat_line(btw_tool_git_status(staged = FALSE)@value) + Output + test.txt [modified] -unstaged + +--- + + Code + cli::cat_line(btw_tool_git_status()@value) + Output + test.txt [modified] -unstaged + +--- + + Code + cli::cat_line(btw_tool_git_status()@value) + Output + test.txt [modified] +staged # btw_tool_git_diff() @@ -44,9 +68,11 @@ Code cli::cat_line(result@value) Output - | commit | author | time | message | - |--------|--------|------|---------| - | abcd123 | Test User | 2025-10-11 12:13:14 | Initial commit | + message: Initial commit + author: Test User + time: 2025-10-11 12:13:14 + n_files: 1 + commit: abcd123 # btw_tool_git_commit() @@ -61,16 +87,14 @@ Code cli::cat_line(result@value) Output - | name | ref | upstream | updated | - |------|-----|----------|---------| - | main | refs/heads/main | NA | 2025-10-11 12:13:14 | + main [2025-10-11 12:13:14] # btw_tool_git_branch_create() Code cli::cat_line(result@value) Output - Created branch 'feature-branch' from 'HEAD' + Created branch `feature-branch` from `HEAD`. # btw_tool_git_branch_checkout() @@ -116,28 +140,45 @@ Code cli::cat_line(log_all@value) Output - | commit | author | time | message | - |--------|--------|------|---------| - | abcd123 | Test User | 2025-10-11 12:13:14 | Update file1 | - | abcd123 | Test User | 2025-10-11 12:13:14 | Add file1 | + message: Update file1 + author: Test User + time: 2025-10-11 12:13:14 + n_files: 1 + commit: abcd123 + + message: Add file1 + author: Test User + time: 2025-10-11 12:13:14 + n_files: 1 + commit: abcd123 --- Code cli::cat_line(status_final@value) Output - | file | status | staged | - |------|--------|--------| - | file3.txt | new | FALSE | + file3.txt [new] -unstaged --- Code cli::cat_line(log_final@value) Output - | commit | author | time | message | - |--------|--------|------|---------| - | abcd123 | Test User | 2025-10-11 12:13:14 | Add file2 | - | abcd123 | Test User | 2025-10-11 12:13:14 | Update file1 | - | abcd123 | Test User | 2025-10-11 12:13:14 | Add file1 | + message: Add file2 + author: Test User + time: 2025-10-11 12:13:14 + n_files: 1 + commit: abcd123 + + message: Update file1 + author: Test User + time: 2025-10-11 12:13:14 + n_files: 1 + commit: abcd123 + + message: Add file1 + author: Test User + time: 2025-10-11 12:13:14 + n_files: 1 + commit: abcd123 diff --git a/tests/testthat/test-tool-git.R b/tests/testthat/test-tool-git.R index 620ae9c7..7c6902df 100644 --- a/tests/testthat/test-tool-git.R +++ b/tests/testthat/test-tool-git.R @@ -40,6 +40,28 @@ test_that("btw_tool_git_status()", { result_unstaged <- btw_tool_git_status(staged = FALSE) expect_btw_tool_result(result_unstaged, has_data = FALSE) expect_match(result_unstaged@value, "No changes") + + cat("\nmore content", file = "test.txt", append = TRUE) + expect_snapshot( + cli::cat_line(btw_tool_git_status()@value), + transform = scrub_git_details + ) + expect_snapshot( + cli::cat_line(btw_tool_git_status(staged = FALSE)@value), + transform = scrub_git_details + ) + + gert::git_commit("Add test.txt") + expect_snapshot( + cli::cat_line(btw_tool_git_status()@value), + transform = scrub_git_details + ) + + gert::git_add("test.txt") + expect_snapshot( + cli::cat_line(btw_tool_git_status()@value), + transform = scrub_git_details + ) }) test_that("btw_tool_git_diff()", { @@ -326,7 +348,7 @@ test_that("git tools work together", { # Extract commit SHA from log data to use in diff commit_sha <- log1@extra$data$commit[1] - expect_true(nchar(commit_sha) == 7) + expect_false(grepl("[^a-f0-9]", commit_sha)) # Should be hex SHA # Workflow 2: Make changes, check diff, stage, commit # Modify file1 @@ -374,8 +396,8 @@ test_that("git tools work together", { # Get commits SHAs from log - verifies we can use log data commit_shas <- log_all@extra$data$commit expect_equal(length(commit_shas), 2) - # Both should be 7-character SHAs - expect_true(all(nchar(commit_shas) == 7)) + # All should be hex SHAs + expect_true(!any(grepl("[^a-f0-9]", commit_shas))) # Workflow 4: Multiple file workflow with status tracking # Add second file From dc8323d9a27b80cffddc660013c5978deccc8234 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 14:04:54 -0400 Subject: [PATCH 10/23] docs(btw_tools): link to tool functions from table --- R/tools.R | 1 + man/btw_tools.Rd | 50 ++++++++++++++++++++++++------------------------ 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/R/tools.R b/R/tools.R index 562b7002..8933300b 100644 --- a/R/tools.R +++ b/R/tools.R @@ -189,6 +189,7 @@ tool_icon <- local({ }) x <- do.call(rbind, x) x <- x[order(x$Group, x$Name), ] + x$Name <- sprintf("[%s()]", x$Name) md_table(x) } # nocov end diff --git a/man/btw_tools.Rd b/man/btw_tools.Rd index bd4135d2..6b2a9bc6 100644 --- a/man/btw_tools.Rd +++ b/man/btw_tools.Rd @@ -31,31 +31,31 @@ 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_git_branch_checkout \tab git \tab Switch to a different git branch. \cr - btw_tool_git_branch_create \tab git \tab Create a new git branch. \cr - btw_tool_git_branch_list \tab git \tab List git branches in the repository. \cr - btw_tool_git_commit \tab git \tab Stage files and create a git commit. \cr - btw_tool_git_diff \tab git \tab View changes in the working directory or between commits. \cr - btw_tool_git_log \tab git \tab Show the commit history for a repository. \cr - btw_tool_git_status \tab git \tab Show the status of the git working directory. \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 + \code{\link[=btw_tool_docs_available_vignettes]{btw_tool_docs_available_vignettes()}} \tab docs \tab List available vignettes for an R package. \cr + \code{\link[=btw_tool_docs_help_page]{btw_tool_docs_help_page()}} \tab docs \tab Get help page from package. \cr + \code{\link[=btw_tool_docs_package_help_topics]{btw_tool_docs_package_help_topics()}} \tab docs \tab Get available help topics for an R package. \cr + \code{\link[=btw_tool_docs_package_news]{btw_tool_docs_package_news()}} \tab docs \tab Read the release notes (NEWS) for a package. \cr + \code{\link[=btw_tool_docs_vignette]{btw_tool_docs_vignette()}} \tab docs \tab Get a package vignette in plain text. \cr + \code{\link[=btw_tool_env_describe_data_frame]{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 + \code{\link[=btw_tool_env_describe_environment]{btw_tool_env_describe_environment()}} \tab env \tab List and describe items in an environment. \cr + \code{\link[=btw_tool_files_code_search]{btw_tool_files_code_search()}} \tab files \tab Search code files in the project. \cr + \code{\link[=btw_tool_files_list_files]{btw_tool_files_list_files()}} \tab 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 files \tab Read an entire text file. \cr + \code{\link[=btw_tool_files_write_text_file]{btw_tool_files_write_text_file()}} \tab files \tab Write content to a text file. \cr + \code{\link[=btw_tool_git_branch_checkout]{btw_tool_git_branch_checkout()}} \tab git \tab Switch to a different git branch. \cr + \code{\link[=btw_tool_git_branch_create]{btw_tool_git_branch_create()}} \tab git \tab Create a new git branch. \cr + \code{\link[=btw_tool_git_branch_list]{btw_tool_git_branch_list()}} \tab git \tab List git branches in the repository. \cr + \code{\link[=btw_tool_git_commit]{btw_tool_git_commit()}} \tab git \tab Stage files and create a git commit. \cr + \code{\link[=btw_tool_git_diff]{btw_tool_git_diff()}} \tab git \tab View changes in the working directory or between commits. \cr + \code{\link[=btw_tool_git_log]{btw_tool_git_log()}} \tab git \tab Show the commit history for a repository. \cr + \code{\link[=btw_tool_git_status]{btw_tool_git_status()}} \tab git \tab Show the status of the git working directory. \cr + \code{\link[=btw_tool_ide_read_current_editor]{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 + \code{\link[=btw_tool_search_package_info]{btw_tool_search_package_info()}} \tab search \tab Describe a CRAN package. \cr + \code{\link[=btw_tool_search_packages]{btw_tool_search_packages()}} \tab search \tab Search for an R package on CRAN. \cr + \code{\link[=btw_tool_session_check_package_installed]{btw_tool_session_check_package_installed()}} \tab session \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 session \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 session \tab Describes the R version, operating system, language and locale settings for the user's system. \cr + \code{\link[=btw_tool_web_read_url]{btw_tool_web_read_url()}} \tab web \tab Read a web page and convert it to Markdown format. \cr } } \examples{ From ffcdcb4fb6cc3398eff1e7f41c4aae2c73124ff3 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 14:49:20 -0400 Subject: [PATCH 11/23] chore(AGENTS): Add check functions --- AGENTS.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) 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 From 4139ec218ae764b469aec83c3dbf4ba75821c5fb Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 15:23:01 -0400 Subject: [PATCH 12/23] refactor: Use HTML from shiny --- R/tool-docs.R | 2 +- R/tool-files.R | 4 ++-- R/tool-git.R | 6 +++--- R/tools.R | 2 +- R/utils.R | 8 -------- 5 files changed, 7 insertions(+), 15 deletions(-) diff --git a/R/tool-docs.R b/R/tool-docs.R index 5036a8ff..9bbef8fb 100644 --- a/R/tool-docs.R +++ b/R/tool-docs.R @@ -188,7 +188,7 @@ btw_tool_docs_help_page_impl <- function(topic, package_name = "") { topic = basename(resolved$topic), package = resolved$package, display = list( - title = HTML(sprintf('?%s', help_call)), + title = shiny::HTML(sprintf('?%s', help_call)), show_request = FALSE, markdown = paste(md, collapse = "\n") ) diff --git a/R/tool-files.R b/R/tool-files.R index 58053a6c..5577b4e3 100644 --- a/R/tool-files.R +++ b/R/tool-files.R @@ -186,7 +186,7 @@ btw_tool_files_read_text_file_impl <- function( path = fs::path_rel(path), display = list( markdown = value, - title = HTML(sprintf( + title = shiny::HTML(sprintf( "Read %s", fs::path_file(path) )), @@ -421,7 +421,7 @@ btw_tool_files_write_text_file_impl <- function(path, content) { previous_content = previous_content, display = list( markdown = md_code_block(fs::path_ext(path), content), - title = HTML(sprintf("Write %s", path)), + title = shiny::HTML(sprintf("Write %s", path)), show_request = FALSE, icon = tool_icon("file-save") ) diff --git a/R/tool-git.R b/R/tool-git.R index a7cb4d5f..aa9b0a6a 100644 --- a/R/tool-git.R +++ b/R/tool-git.R @@ -128,7 +128,7 @@ btw_tool_git_diff_impl <- function(ref = NULL) { value, display = list( markdown = value, - title = HTML(sprintf( + title = shiny::HTML(sprintf( "Git Diff%s", if (!is.null(ref)) sprintf(" (%s)", ref) else "" )) @@ -502,7 +502,7 @@ btw_tool_git_branch_create_impl <- function( result, display = list( markdown = result, - title = htmltools::HTML(sprintf( + title = shiny::HTML(sprintf( "Git Create Branch %s", branch )) @@ -590,7 +590,7 @@ btw_tool_git_branch_checkout_impl <- function( result, display = list( markdown = md_code_block("", result), - title = HTML("Git Checkout") + title = shiny::HTML("Git Checkout") ) ) } diff --git a/R/tools.R b/R/tools.R index 8933300b..c10f13bb 100644 --- a/R/tools.R +++ b/R/tools.R @@ -161,7 +161,7 @@ tool_icon <- local({ return(icons[[name]]) } - icon <- HTML(readLines( + icon <- shiny::HTML(readLines( fs::path_package("btw", "icons", paste0(name, ".svg")), warn = FALSE )) diff --git a/R/utils.R b/R/utils.R index 9e17ff0b..0c241f47 100644 --- a/R/utils.R +++ b/R/utils.R @@ -43,14 +43,6 @@ glue_ <- function(x, ..., .envir = parent.frame()) { as.character(ellmer::interpolate(x, ..., .envir = .envir)) } -HTML <- function(text, ...) { - # In-lined htmltools::HTML() to avoid dependency - x <- paste(c(text, ...), collapse = " ") - attr(x, "html") <- TRUE - class(x) <- c("html", "character") - x -} - # ad-hoc check functions ------------------------------------------------------ check_inherits <- function( x, From 1edce56c628283156b4b4c0b0f3cee2e47bee8b7 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 15:26:43 -0400 Subject: [PATCH 13/23] tests: Use `main` as default branch in temp git repos --- tests/testthat/helpers-git.R | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/testthat/helpers-git.R b/tests/testthat/helpers-git.R index dc36bee6..22bc9c75 100644 --- a/tests/testthat/helpers-git.R +++ b/tests/testthat/helpers-git.R @@ -7,6 +7,7 @@ local_temp_git_repo <- function( withr::local_dir(repo, .local_envir = .local_envir) gert::git_init(".") + gert::git_config_set("init.defaultBranch", "main") gert::git_config_set("user.name", user_name) gert::git_config_set("user.email", user_email) From 93bdd6f1df98d83abe275648092e84ece3c7323c Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 15:31:20 -0400 Subject: [PATCH 14/23] Revert "refactor: Use HTML from shiny" This reverts commit 4139ec218ae764b469aec83c3dbf4ba75821c5fb. --- R/tool-docs.R | 2 +- R/tool-files.R | 4 ++-- R/tool-git.R | 6 +++--- R/tools.R | 2 +- R/utils.R | 8 ++++++++ 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/R/tool-docs.R b/R/tool-docs.R index 9bbef8fb..5036a8ff 100644 --- a/R/tool-docs.R +++ b/R/tool-docs.R @@ -188,7 +188,7 @@ btw_tool_docs_help_page_impl <- function(topic, package_name = "") { topic = basename(resolved$topic), package = resolved$package, display = list( - title = shiny::HTML(sprintf('?%s', help_call)), + title = HTML(sprintf('?%s', help_call)), show_request = FALSE, markdown = paste(md, collapse = "\n") ) diff --git a/R/tool-files.R b/R/tool-files.R index 5577b4e3..58053a6c 100644 --- a/R/tool-files.R +++ b/R/tool-files.R @@ -186,7 +186,7 @@ btw_tool_files_read_text_file_impl <- function( path = fs::path_rel(path), display = list( markdown = value, - title = shiny::HTML(sprintf( + title = HTML(sprintf( "Read %s", fs::path_file(path) )), @@ -421,7 +421,7 @@ btw_tool_files_write_text_file_impl <- function(path, content) { previous_content = previous_content, display = list( markdown = md_code_block(fs::path_ext(path), content), - title = shiny::HTML(sprintf("Write %s", path)), + title = HTML(sprintf("Write %s", path)), show_request = FALSE, icon = tool_icon("file-save") ) diff --git a/R/tool-git.R b/R/tool-git.R index aa9b0a6a..a7cb4d5f 100644 --- a/R/tool-git.R +++ b/R/tool-git.R @@ -128,7 +128,7 @@ btw_tool_git_diff_impl <- function(ref = NULL) { value, display = list( markdown = value, - title = shiny::HTML(sprintf( + title = HTML(sprintf( "Git Diff%s", if (!is.null(ref)) sprintf(" (%s)", ref) else "" )) @@ -502,7 +502,7 @@ btw_tool_git_branch_create_impl <- function( result, display = list( markdown = result, - title = shiny::HTML(sprintf( + title = htmltools::HTML(sprintf( "Git Create Branch %s", branch )) @@ -590,7 +590,7 @@ btw_tool_git_branch_checkout_impl <- function( result, display = list( markdown = md_code_block("", result), - title = shiny::HTML("Git Checkout") + title = HTML("Git Checkout") ) ) } diff --git a/R/tools.R b/R/tools.R index c10f13bb..8933300b 100644 --- a/R/tools.R +++ b/R/tools.R @@ -161,7 +161,7 @@ tool_icon <- local({ return(icons[[name]]) } - icon <- shiny::HTML(readLines( + icon <- HTML(readLines( fs::path_package("btw", "icons", paste0(name, ".svg")), warn = FALSE )) diff --git a/R/utils.R b/R/utils.R index 0c241f47..9e17ff0b 100644 --- a/R/utils.R +++ b/R/utils.R @@ -43,6 +43,14 @@ glue_ <- function(x, ..., .envir = parent.frame()) { as.character(ellmer::interpolate(x, ..., .envir = .envir)) } +HTML <- function(text, ...) { + # In-lined htmltools::HTML() to avoid dependency + x <- paste(c(text, ...), collapse = " ") + attr(x, "html") <- TRUE + class(x) <- c("html", "character") + x +} + # ad-hoc check functions ------------------------------------------------------ check_inherits <- function( x, From a1913d16d57709fbfe0687e8a516d7023aa2fda5 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 15:31:40 -0400 Subject: [PATCH 15/23] chore: Use local version of HTML() --- R/tool-git.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/tool-git.R b/R/tool-git.R index a7cb4d5f..9403bc14 100644 --- a/R/tool-git.R +++ b/R/tool-git.R @@ -502,7 +502,7 @@ btw_tool_git_branch_create_impl <- function( result, display = list( markdown = result, - title = htmltools::HTML(sprintf( + title = HTML(sprintf( "Git Create Branch %s", branch )) From 5f5d2fda4614da601a21d942b1d0e86998a1947f Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 16:46:06 -0400 Subject: [PATCH 16/23] tests: Really force `main` over `master` --- tests/testthat/helpers-git.R | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/testthat/helpers-git.R b/tests/testthat/helpers-git.R index 22bc9c75..55be3554 100644 --- a/tests/testthat/helpers-git.R +++ b/tests/testthat/helpers-git.R @@ -7,7 +7,10 @@ local_temp_git_repo <- function( withr::local_dir(repo, .local_envir = .local_envir) gert::git_init(".") + # Set default branch to 'main' instead of 'master' gert::git_config_set("init.defaultBranch", "main") + . <- system("git symbolic-ref HEAD refs/heads/main", intern = TRUE) + gert::git_config_set("user.name", user_name) gert::git_config_set("user.email", user_email) From bfeb855baf775f3be3489cdb85a70ecafdc6d550 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 21:30:23 -0400 Subject: [PATCH 17/23] git: clarify git-diff behavior and fix empty-diff detection Use gert::git_diff_patch param docs and fix empty-diff detection (handle character vectors returned by git_diff_patch). Update tool description and man pages to clarify that the tool shows the diff for a single commit and note the limitation that diffing two arbitrary commits is not supported. --- R/tool-git.R | 18 ++++++++---------- man/btw_tool_git_diff.Rd | 6 ++---- man/btw_tools.Rd | 2 +- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/R/tool-git.R b/R/tool-git.R index 9403bc14..ccde01cf 100644 --- a/R/tool-git.R +++ b/R/tool-git.R @@ -94,10 +94,7 @@ RETURNS: A list of file paths, their status (new, modified, deleted, etc.), and #' btw_tool_git_diff(ref = "HEAD") #' } #' -#' @param ref Optional reference such as `"HEAD"`, a commit SHA, or two refs -#' separated by `..` (e.g., `"main..feature"`). Use `NULL` (default) to diff -#' the working directory against the repository index (unstaged changes). -#' Use `"HEAD"` to see staged changes. +#' @inheritParams gert::git_diff_patch #' @inheritParams btw_tool_docs_package_news #' #' @return Returns a diff patch as a formatted string. @@ -112,7 +109,7 @@ btw_tool_git_diff_impl <- function(ref = NULL) { diff_patch <- gert::git_diff_patch(ref = ref) - if (length(diff_patch) == 0 || nzchar(diff_patch) == FALSE) { + if (length(diff_patch) == 0 || all(!nzchar(diff_patch))) { msg <- if (is.null(ref)) { "No unstaged changes to show" } else { @@ -143,15 +140,16 @@ btw_tool_git_diff_impl <- function(ref = NULL) { ellmer::tool( btw_tool_git_diff_impl, name = "btw_tool_git_diff", - description = r"---(View changes in the working directory or between commits. + 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 = "commit1..commit2"` to compare two commits. -* Use with a branch name to compare current HEAD with that branch. +* Use with `ref = "commit_sha"` to see the diff of that commit. -RETURNS: A unified diff patch showing the changes. +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", @@ -162,7 +160,7 @@ RETURNS: A unified diff patch showing the changes. ), arguments = list( ref = ellmer::type_string( - 'Optional reference like "HEAD", a commit SHA, or "ref1..ref2". NULL (default) shows unstaged changes.', + 'Optional reference like "HEAD" or a commit id. `null` (default) shows unstaged changes.', required = FALSE ) ) diff --git a/man/btw_tool_git_diff.Rd b/man/btw_tool_git_diff.Rd index a5f339e8..375405a9 100644 --- a/man/btw_tool_git_diff.Rd +++ b/man/btw_tool_git_diff.Rd @@ -7,10 +7,8 @@ btw_tool_git_diff(ref = NULL, `_intent` = "") } \arguments{ -\item{ref}{Optional reference such as \code{"HEAD"}, a commit SHA, or two refs -separated by \code{..} (e.g., \code{"main..feature"}). Use \code{NULL} (default) to diff -the working directory against the repository index (unstaged changes). -Use \code{"HEAD"} to see staged changes.} +\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 diff --git a/man/btw_tools.Rd b/man/btw_tools.Rd index 6b2a9bc6..54fe0152 100644 --- a/man/btw_tools.Rd +++ b/man/btw_tools.Rd @@ -46,7 +46,7 @@ this function have access to the tools:\tabular{lll}{ \code{\link[=btw_tool_git_branch_create]{btw_tool_git_branch_create()}} \tab git \tab Create a new git branch. \cr \code{\link[=btw_tool_git_branch_list]{btw_tool_git_branch_list()}} \tab git \tab List git branches in the repository. \cr \code{\link[=btw_tool_git_commit]{btw_tool_git_commit()}} \tab git \tab Stage files and create a git commit. \cr - \code{\link[=btw_tool_git_diff]{btw_tool_git_diff()}} \tab git \tab View changes in the working directory or between commits. \cr + \code{\link[=btw_tool_git_diff]{btw_tool_git_diff()}} \tab git \tab View changes in the working directory or a commit. \cr \code{\link[=btw_tool_git_log]{btw_tool_git_log()}} \tab git \tab Show the commit history for a repository. \cr \code{\link[=btw_tool_git_status]{btw_tool_git_status()}} \tab git \tab Show the status of the git working directory. \cr \code{\link[=btw_tool_ide_read_current_editor]{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 From ed76502044bec3d000a7e39dca7caee50a97bad9 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 21:30:32 -0400 Subject: [PATCH 18/23] ui: add tool-group toggle and centralized group icons Add clickable action-link toggle to each tool group so users can select/deselect all tools in a group from the btw_app UI. Introduce tool_group_icon() and refactor set_tool_icon() to centralize group icons. Add a new 'difference' SVG icon used for the git group. --- R/btw_client_app.R | 46 +++++++++++++++++++++++++++++---------- R/tools.R | 12 +++++----- inst/icons/difference.svg | 1 + 3 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 inst/icons/difference.svg diff --git a/R/btw_client_app.R b/R/btw_client_app.R index db77a993..5dd0afdf 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,23 +279,14 @@ 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"), @@ -283,7 +297,15 @@ app_tool_group_choice_input <- function( 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/tools.R b/R/tools.R index 8933300b..151aa97d 100644 --- a/R/tools.R +++ b/R/tools.R @@ -132,25 +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("source-environment"), + "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 } 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 @@ + From 45583eefa1ced5677c3031d5ebd65492b9b6d92b Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Oct 2025 21:43:00 -0400 Subject: [PATCH 19/23] refactor(git-status): Rename `staged` to `include` Use enum include for git status: switch staged -> include; update tests and snapshot --- R/tool-git.R | 22 +++++++++++++++------- man/btw_tool_git_status.Rd | 10 +++++++--- tests/testthat/_snaps/tool-git.md | 2 +- tests/testthat/test-tool-git.R | 10 +++++----- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/R/tool-git.R b/R/tool-git.R index ccde01cf..5c5df267 100644 --- a/R/tool-git.R +++ b/R/tool-git.R @@ -10,8 +10,8 @@ NULL #' btw_tool_git_status() #' } #' -#' @param staged Optional. Return only staged (`TRUE`) or unstaged files -#' (`FALSE`). Use `NULL` to show both (default). +#' @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 #' @@ -19,16 +19,23 @@ NULL #' #' @family git tools #' @export -btw_tool_git_status <- function(staged, pathspec, `_intent`) {} +btw_tool_git_status <- function(include, pathspec, `_intent`) {} btw_tool_git_status_impl <- function( - staged = NULL, + include = c("both", "staged", "unstaged"), pathspec = NULL ) { check_installed("gert") - check_bool(staged, allow_null = TRUE) + 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) { @@ -70,8 +77,9 @@ RETURNS: A list of file paths, their status (new, modified, deleted, etc.), and btw_can_register = function() rlang::is_installed("gert") ), arguments = list( - staged = ellmer::type_boolean( - "Optional. Return only staged (true) or unstaged files (false). Use null to show both (default).", + 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( diff --git a/man/btw_tool_git_status.Rd b/man/btw_tool_git_status.Rd index fa11f852..e7506362 100644 --- a/man/btw_tool_git_status.Rd +++ b/man/btw_tool_git_status.Rd @@ -4,11 +4,15 @@ \alias{btw_tool_git_status} \title{Tool: Git Status} \usage{ -btw_tool_git_status(staged = NULL, pathspec = NULL, `_intent` = "") +btw_tool_git_status( + include = c("both", "staged", "unstaged"), + pathspec = NULL, + `_intent` = "" +) } \arguments{ -\item{staged}{Optional. Return only staged (\code{TRUE}) or unstaged files -(\code{FALSE}). Use \code{NULL} to show both (default).} +\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.} diff --git a/tests/testthat/_snaps/tool-git.md b/tests/testthat/_snaps/tool-git.md index 435e1e0c..de8abf1e 100644 --- a/tests/testthat/_snaps/tool-git.md +++ b/tests/testthat/_snaps/tool-git.md @@ -29,7 +29,7 @@ --- Code - cli::cat_line(btw_tool_git_status(staged = FALSE)@value) + cli::cat_line(btw_tool_git_status(include = "unstaged")@value) Output test.txt [modified] -unstaged diff --git a/tests/testthat/test-tool-git.R b/tests/testthat/test-tool-git.R index 7c6902df..819e7bf6 100644 --- a/tests/testthat/test-tool-git.R +++ b/tests/testthat/test-tool-git.R @@ -17,7 +17,7 @@ test_that("btw_tool_git_status()", { expect_snapshot(cli::cat_line(result@value), transform = scrub_git_details) # Check staged only (should be empty) - result_staged_empty <- btw_tool_git_status(staged = TRUE) + result_staged_empty <- btw_tool_git_status(include = "staged") expect_btw_tool_result(result_staged_empty, has_data = FALSE) expect_match(result_staged_empty@value, "No changes") expect_snapshot( @@ -29,7 +29,7 @@ test_that("btw_tool_git_status()", { gert::git_add("test.txt") # Check staged only - result_staged <- btw_tool_git_status(staged = TRUE) + result_staged <- btw_tool_git_status(include = "staged") expect_match(result_staged@value, "test\\.txt") expect_snapshot( cli::cat_line(result_staged@value), @@ -37,7 +37,7 @@ test_that("btw_tool_git_status()", { ) # Check unstaged only (should be empty) - result_unstaged <- btw_tool_git_status(staged = FALSE) + result_unstaged <- btw_tool_git_status(include = "unstaged") expect_btw_tool_result(result_unstaged, has_data = FALSE) expect_match(result_unstaged@value, "No changes") @@ -47,7 +47,7 @@ test_that("btw_tool_git_status()", { transform = scrub_git_details ) expect_snapshot( - cli::cat_line(btw_tool_git_status(staged = FALSE)@value), + cli::cat_line(btw_tool_git_status(include = "unstaged")@value), transform = scrub_git_details ) @@ -291,7 +291,7 @@ test_that("git tools validate arguments", { local_temp_git_repo() # btw_tool_git_status - expect_error(btw_tool_git_status(staged = "invalid")) + expect_error(btw_tool_git_status(include = "invalid")) expect_error(btw_tool_git_status(pathspec = 123)) # btw_tool_git_diff From c8fabd46b0ddf2fe606227e3e0ef8698370bfe6b Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Oct 2025 06:53:00 -0400 Subject: [PATCH 20/23] refactor: to_title_case --- R/btw_client_app.R | 2 +- R/utils.R | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/R/btw_client_app.R b/R/btw_client_app.R index 5dd0afdf..384bb4ff 100644 --- a/R/btw_client_app.R +++ b/R/btw_client_app.R @@ -292,7 +292,7 @@ app_tool_group_choice_input <- function( "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( 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)) +} From cd0d06f6d0a777d4901d0440f65f8045f242d551 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Oct 2025 06:53:44 -0400 Subject: [PATCH 21/23] docs(btw_tools): break tools into groups --- R/tools.R | 9 ++++- man/btw_tools.Rd | 99 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 80 insertions(+), 28 deletions(-) diff --git a/R/tools.R b/R/tools.R index 151aa97d..2f817b87 100644 --- a/R/tools.R +++ b/R/tools.R @@ -192,6 +192,13 @@ tool_icon <- local({ x <- do.call(rbind, x) x <- x[order(x$Group, x$Name), ] x$Name <- sprintf("[%s()]", x$Name) - md_table(x) + + 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/man/btw_tools.Rd b/man/btw_tools.Rd index 54fe0152..bb420c04 100644 --- a/man/btw_tools.Rd +++ b/man/btw_tools.Rd @@ -29,33 +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 - \code{\link[=btw_tool_docs_available_vignettes]{btw_tool_docs_available_vignettes()}} \tab docs \tab List available vignettes for an R package. \cr - \code{\link[=btw_tool_docs_help_page]{btw_tool_docs_help_page()}} \tab docs \tab Get help page from package. \cr - \code{\link[=btw_tool_docs_package_help_topics]{btw_tool_docs_package_help_topics()}} \tab docs \tab Get available help topics for an R package. \cr - \code{\link[=btw_tool_docs_package_news]{btw_tool_docs_package_news()}} \tab docs \tab Read the release notes (NEWS) for a package. \cr - \code{\link[=btw_tool_docs_vignette]{btw_tool_docs_vignette()}} \tab docs \tab Get a package vignette in plain text. \cr - \code{\link[=btw_tool_env_describe_data_frame]{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 - \code{\link[=btw_tool_env_describe_environment]{btw_tool_env_describe_environment()}} \tab env \tab List and describe items in an environment. \cr - \code{\link[=btw_tool_files_code_search]{btw_tool_files_code_search()}} \tab files \tab Search code files in the project. \cr - \code{\link[=btw_tool_files_list_files]{btw_tool_files_list_files()}} \tab 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 files \tab Read an entire text file. \cr - \code{\link[=btw_tool_files_write_text_file]{btw_tool_files_write_text_file()}} \tab files \tab Write content to a text file. \cr - \code{\link[=btw_tool_git_branch_checkout]{btw_tool_git_branch_checkout()}} \tab git \tab Switch to a different git branch. \cr - \code{\link[=btw_tool_git_branch_create]{btw_tool_git_branch_create()}} \tab git \tab Create a new git branch. \cr - \code{\link[=btw_tool_git_branch_list]{btw_tool_git_branch_list()}} \tab git \tab List git branches in the repository. \cr - \code{\link[=btw_tool_git_commit]{btw_tool_git_commit()}} \tab git \tab Stage files and create a git commit. \cr - \code{\link[=btw_tool_git_diff]{btw_tool_git_diff()}} \tab git \tab View changes in the working directory or a commit. \cr - \code{\link[=btw_tool_git_log]{btw_tool_git_log()}} \tab git \tab Show the commit history for a repository. \cr - \code{\link[=btw_tool_git_status]{btw_tool_git_status()}} \tab git \tab Show the status of the git working directory. \cr - \code{\link[=btw_tool_ide_read_current_editor]{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 - \code{\link[=btw_tool_search_package_info]{btw_tool_search_package_info()}} \tab search \tab Describe a CRAN package. \cr - \code{\link[=btw_tool_search_packages]{btw_tool_search_packages()}} \tab search \tab Search for an R package on CRAN. \cr - \code{\link[=btw_tool_session_check_package_installed]{btw_tool_session_check_package_installed()}} \tab session \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 session \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 session \tab Describes the R version, operating system, language and locale settings for the user's system. \cr - \code{\link[=btw_tool_web_read_url]{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{ From e8a07713d3a9036e59b092c6186868304fb77f71 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Oct 2025 06:55:13 -0400 Subject: [PATCH 22/23] chore: Add `git` tool group to btw.md template --- R/edit_btw_md.R | 2 +- inst/templates/btw.md | 2 +- man/use_btw_md.Rd | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/inst/templates/btw.md b/inst/templates/btw.md index 21d76305..357dcfe6 100644 --- a/inst/templates/btw.md +++ b/inst/templates/btw.md @@ -1,6 +1,6 @@ --- 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/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{
}}\preformatted{--- 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: From 24af3060d7ded297ea7dce8b91d9a698cd331d20 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Oct 2025 06:57:23 -0400 Subject: [PATCH 23/23] chore: write out tools as long list (easier to toggle with comments) --- inst/templates/btw.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/inst/templates/btw.md b/inst/templates/btw.md index 357dcfe6..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, git, ide, search, session, web] +tools: + - docs + - env + - files + - git + - ide + - search + - session + - web --- Follow these important style rules when writing R code: