diff --git a/DESCRIPTION b/DESCRIPTION
index fc7d3aca..9ed1eaef 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -52,7 +52,7 @@ Imports:
withr,
xml2
Suggests:
- bslib (>= 0.7.0),
+ bslib (>= 0.11.0),
callr,
chromote,
covr,
@@ -73,7 +73,7 @@ Suggests:
renv,
roxygen2,
shiny,
- shinychat (>= 0.3.0),
+ shinychat (>= 0.3.0.9000),
testthat (>= 3.0.0),
tibble,
usethis
@@ -137,3 +137,5 @@ Collate:
'utils-r.R'
'utils.R'
'zzz.R'
+Remotes:
+ posit-dev/shinychat
diff --git a/R/btw_client_app.R b/R/btw_client_app.R
index 0b147a03..9f946616 100644
--- a/R/btw_client_app.R
+++ b/R/btw_client_app.R
@@ -4,27 +4,37 @@
#' chat
#' @param messages A list of initial messages to show in the chat, passed to
#' [shinychat::chat_mod_ui()].
+#' @param model_choices Can be one of `"btw_md"` (model choices from your
+#' `path_btw` configuration), `"provider"` (models from the provider API),
+#' `"auto"` (uses `path_btw` if `client` comes from `path_btw`, otherwise
+#' falling back to provider), or `"none"` (don't show model choices).
#' @export
btw_app <- function(
...,
client = NULL,
tools = NULL,
path_btw = NULL,
- messages = list()
+ messages = list(),
+ model_choices = c("auto", "btw_md", "provider", "none")
) {
rlang::check_installed("shiny")
- rlang::check_installed("bslib")
+ rlang::check_installed("bslib", version = "0.11.0")
rlang::check_installed("htmltools")
- rlang::check_installed("shinychat", version = "0.3.0")
+ rlang::check_installed("shinychat", version = "0.3.0.9000")
+
+ model_choices <- rlang::arg_match(model_choices)
if (getOption("btw.app.close_on_session_end", FALSE)) {
cli::cli_alert("Starting up {.fn btw::btw_app} ...")
}
+ client_name <- if (is_string(client)) client
+
# Get reference tools for the app
if (inherits(client, "AsIs")) {
# When client is AsIs (pre-configured), use btw_tools() as reference
reference_tools <- btw_tools()
+ app_models <- app_resolve_model_choices(model_choices, path_btw = FALSE)
} else {
client <- btw_client(
client = client,
@@ -44,12 +54,24 @@ btw_app <- function(
)
reference_tools <- reference_client$get_tools()
})
+
+ app_models <- app_resolve_model_choices(
+ model_choices,
+ path_btw,
+ client_name = client_name
+ )
+ }
+
+ selected_client <- if (is.list(app_models) && !is.null(client_name)) {
+ resolve_model_choice_name(client_name, names(app_models))
}
btw_app_from_client(
client,
messages = messages,
allowed_tools = reference_tools,
+ app_models = app_models,
+ selected_client = selected_client,
...
)
}
@@ -72,18 +94,14 @@ btw_app_from_client <- function(
client,
messages = list(),
allowed_tools = btw_tools(),
+ app_models = "provider",
+ selected_client = NULL,
...
) {
path_figures_installed <- system.file("help", "figures", package = "btw")
path_figures_dev <- system.file("man", "figures", package = "btw")
path_logo <- "btw_figures/logo.png"
- provider_model <- sprintf(
- "%s/%s",
- client$get_provider()@name,
- client$get_model()
- )
-
# Store original client tools (preserves configuration like closures)
# $get_tools() returns a named list where names are tool names
original_client_tools <- client$get_tools()
@@ -179,11 +197,16 @@ btw_app_from_client <- function(
shinychat::chat_mod_ui(
"chat",
messages = messages,
- width = "min(750px, 100%)"
+ width = "min(750px, 100%)",
+ footer = if (utils::packageVersion("shinychat") >= "0.3.0.9000") {
+ btw_status_bar_ui(
+ "status_bar",
+ client = client,
+ models = app_models,
+ selected = selected_client
+ )
+ }
),
- if (utils::packageVersion("shinychat") >= "0.2.0.9000") {
- btw_status_bar_ui("status_bar", provider_model)
- },
btw_app_html_dep(),
)
}
@@ -191,8 +214,12 @@ btw_app_from_client <- function(
server <- function(input, output, session) {
chat <- shinychat::chat_mod_server("chat", client = client)
- if (utils::packageVersion("shinychat") >= "0.2.0.9000") {
- btw_status_bar_server("status_bar", chat)
+ if (utils::packageVersion("shinychat") >= "0.3.0.9000") {
+ res <- btw_status_bar_server("status_bar", chat, app_models)
+
+ shiny::observeEvent(res$clear_chat(), {
+ chat$clear(client_history = "clear")
+ })
}
shiny::observeEvent(input$show_sidebar, {
@@ -470,60 +497,176 @@ notifier <- function(icon, action, error = NULL, ...) {
bslib_show_toast(toast)
}
-btw_status_bar_ui <- function(id, provider_model) {
+btw_status_bar_ui <- function(
+ id,
+ client,
+ models = "provider",
+ selected = NULL
+) {
ns <- shiny::NS(id)
+
+ if (identical(models, "provider")) {
+ selected <- client$get_model()
+ choices <- selected # full list populated asynchronously in server
+ } else if (length(models) > 0) {
+ selected <- selected %||% names(models)[[1]]
+ choices <- names(models)
+ }
+
shiny::tagList(
shiny::tags$footer(
- class = "status-footer d-flex align-items-center gap-3 small text-muted",
+ class = "status-footer small text-muted",
style = "width: min(725px, 100%); margin-inline: auto;",
- bslib::tooltip(
- shiny::actionLink(ns("show_sys_prompt"), tool_icon("quick-reference")),
- "Show system prompt"
- ),
- shiny::div(
- class = "status-provider-model",
- shiny::span(class = "font-monospace", provider_model),
- ),
- shiny::div(
- class = "ms-auto status-tokens font-monospace",
- bslib::tooltip(
- id = ns("status_tokens_input_tooltip"),
- shiny::span(
- id = ns("status_tokens_input"),
- class = "status-countup",
- "data-type" = "tokens_input"
- ),
- "Input tokens"
+ bslib::toolbar(
+ gap = "0.25em",
+ shiny::uiOutput(ns("provider")),
+ if (is.null(models)) {
+ shiny::div(
+ class = "status-model badge text-body-secondary fw-normal",
+ client$get_model()
+ )
+ } else {
+ bslib::toolbar_input_select(
+ id = ns("model"),
+ label = "Model",
+ selected = selected,
+ choices = choices,
+ style = bslib::css(min_width = "12rem")
+ )
+ },
+ bslib::toolbar_spacer(),
+ bslib::toolbar_input_button(
+ id = ns("show_sys_prompt"),
+ label = "Show system prompt",
+ icon = tool_icon("quick-reference")
),
- bslib::tooltip(
- shiny::span(
- id = ns("status_tokens_output"),
- class = "status-countup",
- "data-type" = "tokens_output"
- ),
- "Output tokens"
- )
- ),
- shiny::div(
- class = "status-cost font-monospace",
- bslib::tooltip(
- id = ns("status_cost_tooltip"),
- shiny::span(
- id = ns("status_cost"),
- class = "status-countup",
- "data-type" = "cost"
+ bslib::toolbar_input_button(
+ id = ns("clear_chat"),
+ label = "Clear chat",
+ icon = tool_icon("ink-eraser"),
+ ),
+ bslib::toolbar_divider(),
+ shiny::div(
+ class = "status-tokens font-monospace",
+ bslib::tooltip(
+ id = ns("status_tokens_input_tooltip"),
+ shiny::span(
+ id = ns("status_tokens_input"),
+ class = "status-countup",
+ "data-type" = "tokens_input"
+ ),
+ "Input tokens"
),
- "Estimated cost"
- )
+ bslib::tooltip(
+ shiny::span(
+ id = ns("status_tokens_output"),
+ class = "status-countup",
+ "data-type" = "tokens_output"
+ ),
+ "Output tokens"
+ )
+ ),
+ shiny::div(
+ class = "status-cost font-monospace",
+ bslib::tooltip(
+ id = ns("status_cost_tooltip"),
+ shiny::span(
+ id = ns("status_cost"),
+ class = "status-countup",
+ "data-type" = "cost"
+ ),
+ "Estimated cost"
+ )
+ ),
+ width = "100%"
)
)
)
}
-btw_status_bar_server <- function(id, chat) {
+btw_status_bar_server <- function(id, chat, models = "provider") {
shiny::moduleServer(
id,
function(input, output, session) {
+ provider_name <- shiny::reactiveVal({
+ # chat$client is not reactive, will be updated manually on model change
+ chat$client$get_provider()@name
+ })
+
+ model_name <- shiny::reactiveVal({
+ chat$client$get_model()
+ })
+
+ if (identical(models, "provider")) {
+ shiny::observe({
+ provider_df <- client_get_models(chat$client)
+ if (!is.null(provider_df)) {
+ current <- shiny::isolate(model_name())
+ all_choices <- union(current, sort(provider_df$id))
+ shiny::updateSelectInput(session, "model", choices = all_choices, selected = current)
+ }
+ })
+ }
+
+ output$provider <- shiny::renderUI({
+ badge <- shiny::div(
+ class = "status-provider badge",
+ provider_name()
+ )
+ if (identical(models, "provider")) {
+ badge
+ } else {
+ bslib::tooltip(badge, model_name(), placement = "top")
+ }
+ })
+
+ shiny::observeEvent(input$model, ignoreInit = TRUE, {
+ tryCatch(
+ {
+ old_provider <- chat$client$get_provider()@name
+
+ if (identical(models, "provider")) {
+ new_client <- chat$client$clone()
+ new_client$set_model(input$model)
+ } else {
+ new_config <- models[[input$model]]
+ new_client <- btw_client(client = new_config, tools = FALSE)
+ new_client$set_system_prompt(chat$client$get_system_prompt())
+ turns <- chat$client$get_turns()
+ new_provider <- new_client$get_provider()@name
+ if (!identical(old_provider, new_provider)) {
+ turns <- turns_replace_thinking(turns)
+ }
+ new_client$set_turns(turns)
+ new_client$set_tools(chat$client$get_tools())
+ }
+
+ chat$set_client(new_client, sync = FALSE)
+ new_provider <- chat$client$get_provider()@name
+ provider_name(new_provider)
+ model_name(chat$client$get_model())
+
+ notifier(
+ shiny::icon("check"),
+ shiny::HTML(
+ sprintf(
+ "Switched model to %s from %s.",
+ new_client$get_model(),
+ new_provider
+ )
+ )
+ )
+ },
+ error = function(err) {
+ notifier(
+ shiny::icon("warning"),
+ sprintf("Failed to switch model to %s", input$model),
+ error = err
+ )
+ }
+ )
+ })
+
chat_tokens <- shiny::reactiveVal(
chat_get_tokens(chat$client),
label = "btw_app_tokens"
@@ -559,6 +702,10 @@ btw_status_bar_server <- function(id, chat) {
}
})
+ shiny::observeEvent(input$clear_chat, {
+ session$sendCustomMessage("btw_reset_status", list(ns = session$ns("")))
+ })
+
shiny::observeEvent(chat_tokens(), {
tokens <- chat_tokens()
@@ -683,6 +830,12 @@ btw_status_bar_server <- function(id, chat) {
)
}
)
+
+ return(
+ list(
+ clear_chat = reactive(input$clear_chat)
+ )
+ )
}
)
}
diff --git a/R/utils-ellmer.R b/R/utils-ellmer.R
index e8ea6cde..b395cd92 100644
--- a/R/utils-ellmer.R
+++ b/R/utils-ellmer.R
@@ -1,3 +1,168 @@
+client_get_models <- function(client) {
+ provider <- client$get_provider()
+
+ models_fns <- list(
+ ProviderAnthropic = function(p) {
+ ellmer::models_anthropic(
+ base_url = p@base_url,
+ credentials = p@credentials
+ )
+ },
+ ProviderGoogleGemini = function(p) {
+ ellmer::models_google_gemini(
+ base_url = p@base_url,
+ credentials = p@credentials
+ )
+ },
+ ProviderAWSBedrock = function(p) {
+ base_url <- sub("bedrock-runtime", "bedrock", p@base_url)
+ ellmer::models_aws_bedrock(profile = p@profile, base_url = base_url)
+ },
+ ProviderOpenAI = function(p) {
+ ellmer::models_openai(base_url = p@base_url, credentials = p@credentials)
+ },
+ ProviderMistral = function(p) {
+ ellmer::models_mistral()
+ },
+ ProviderLMStudio = function(p) {
+ base_url <- sub("/v1$", "", p@base_url)
+ ellmer::models_lmstudio(base_url = base_url, credentials = p@credentials)
+ },
+ ProviderVllm = function(p) {
+ ellmer::models_vllm(base_url = p@base_url, credentials = p@credentials)
+ },
+ ProviderOllama = function(p) {
+ base_url <- sub("/v1$", "", p@base_url)
+ ellmer::models_ollama(base_url = base_url, credentials = p@credentials)
+ },
+ ProviderPortkeyAI = function(p) {
+ ellmer::models_portkey(base_url = p@base_url)
+ },
+ ProviderOpenAICompatible = function(p) {
+ base_url <- sub("/v1$", "", p@base_url)
+ ellmer::models_openai(base_url = p@base_url, credentials = p@credentials)
+ }
+ )
+
+ try_get_models <- function(fn, provider) {
+ tryCatch(fn(provider), error = function(e) {
+ cli::cli_warn(
+ "Failed to fetch models for provider {provider@name}",
+ parent = e
+ )
+ NULL
+ })
+ }
+
+ if (provider@name == "LM Studio") {
+ return(try_get_models(models_fns$ProviderLMStudio, provider))
+ }
+
+ for (cls in names(models_fns)) {
+ if (inherits(provider, sprintf("ellmer::%s", cls))) {
+ return(
+ tryCatch(models_fns[[cls]](provider), error = function(e) {
+ cli::cli_warn(
+ "Failed to fetch models for provider {provider@name}",
+ parent = e
+ )
+ NULL
+ })
+ )
+ }
+ }
+
+ NULL
+}
+
+# Returns NULL (no selector), "provider" (lazy fetch), or list of btw.md client configs
+app_resolve_model_choices <- function(
+ model_choices,
+ path_btw,
+ client_name = NULL
+) {
+ if (model_choices == "none") {
+ return(NULL)
+ }
+ if (model_choices == "provider") {
+ return("provider")
+ }
+
+ config <- read_btw_file(path_btw)
+ btw_models <- config$client
+
+ if (is.null(btw_models)) {
+ return("provider")
+ }
+
+ if (is_list(btw_models)) {
+ if (all(nzchar(names2(btw_models)))) {
+ if (
+ model_choices == "auto" &&
+ !is.null(client_name) &&
+ is.null(resolve_model_choice_name(client_name, names(btw_models)))
+ ) {
+ return("provider")
+ }
+ return(btw_models)
+ }
+ cli::cli_inform(
+ "Model choices in `client` in {.path {btw_md}} must be named for model selection to work."
+ )
+ }
+
+ "provider"
+}
+
+resolve_model_choice_name <- function(name, choices) {
+ idx <- match(tolower(name), tolower(choices))
+ if (is.na(idx)) NULL else choices[[idx]]
+}
+
+client_models_from_config <- function(client_config) {
+ aliases <- client_aliases(client_config)
+ if (is.null(aliases)) {
+ return(NULL)
+ }
+
+ model_ids <- vapply(
+ client_config,
+ function(cfg) {
+ if (is_string(cfg)) {
+ parts <- strsplit(cfg, "/", fixed = TRUE)[[1]]
+ if (length(parts) > 1) paste(parts[-1], collapse = "/") else ""
+ } else if (is.list(cfg) && !inherits(cfg, "Chat")) {
+ cfg$model %||% ""
+ } else if (inherits(cfg, "Chat")) {
+ cfg$get_model()
+ } else {
+ ""
+ }
+ },
+ character(1)
+ )
+
+ valid <- nzchar(model_ids)
+ if (!any(valid)) {
+ return(NULL)
+ }
+
+ model_ids[valid]
+}
+
+turns_replace_thinking <- function(turns) {
+ lapply(turns, function(turn) {
+ turn@contents <- lapply(turn@contents, function(content) {
+ if (S7::S7_inherits(content, ellmer::ContentThinking)) {
+ ellmer::ContentText(format(content))
+ } else {
+ content
+ }
+ })
+ turn
+ })
+}
+
btw_prompt <- function(path, ..., .envir = parent.frame()) {
path <- system.file("prompts", path, package = "btw")
ellmer::interpolate_file(path, ..., .envir = .envir)
@@ -78,7 +243,8 @@ BtwToolBuiltIn <- tryCatch(
)
built_in_tool_info <- function(name) {
- switch(name,
+ switch(
+ name,
web_search = list(
title = "Web Search",
description = "Search the web for up-to-date information.",
diff --git a/R/zzz.R b/R/zzz.R
index 5d09c061..6ad5346c 100644
--- a/R/zzz.R
+++ b/R/zzz.R
@@ -7,6 +7,18 @@
assign(tool_def@name, tool_def, envir = pkg_env)
}
+ # Patch ellmer:::Chat to add set_model() if it doesn't exist
+ ellmer_chat <- getFromNamespace("Chat", "ellmer")
+ if (!is.null(ellmer_chat)) {
+ if (!"set_model" %in% names(ellmer_chat$public_methods)) {
+ ellmer_chat$set("public", "set_model", function(model) {
+ old <- private$provider@model
+ private$provider@model <- model
+ invisible(old)
+ })
+ }
+ }
+
rlang::run_on_load()
}
diff --git a/btw.md b/btw.md
deleted file mode 100644
index 6439220c..00000000
--- a/btw.md
+++ /dev/null
@@ -1,136 +0,0 @@
----
-client:
- sonnet:
- provider: aws_bedrock
- model: us.anthropic.claude-sonnet-4-6
- api_args:
- additionalModelRequestFields:
- thinking:
- type: enabled
- budget_tokens: 4000
- haiku:
- provider: aws_bedrock
- model: us.anthropic.claude-haiku-4-5-20251001-v1:0
- api_args:
- additionalModelRequestFields:
- thinking:
- type: enabled
- budget_tokens: 4000
- opus:
- provider: aws_bedrock
- model: us.anthropic.claude-opus-4-5-20251101-v1:0
- api_args:
- additionalModelRequestFields:
- thinking:
- type: enabled
- budget_tokens: 4000
-
- sonnet-4.5:
- provider: aws_bedrock
- model: us.anthropic.claude-sonnet-4-5-20250929-v1:0
- sonnet-4:
- provider: aws_bedrock
- model: us.anthropic.claude-sonnet-4-20250514-v1:0
- opus-4.5:
- provider: aws_bedrock
- model: us.anthropic.claude-opus-4-5-20251101-v1:0
-
- gemma4:
- provider: openai_compatible
- model: google/gemma-4-26b-a4b
- # base_url: http://127.0.0.1:1234/v1
- base_url: http://remy.local:1234/v1
- name: "LM Studio"
-
- qwen3.6:
- provider: openai_compatible
- model: qwen/qwen3.6-35b-a3b
- base_url: http://remy.local:1234/v1
- name: "LM Studio"
- preserve_thinking: true
-
- qwen-3-5-35b:
- provider: openai_compatible
- model: qwen/qwen3.5-35b-a3b
- base_url: http://127.0.0.1:1234/v1
- name: "LM Studio"
-
- qwen-3-5-9b:
- provider: openai_compatible
- model: qwen/qwen3.5-9b
- base_url: http://127.0.0.1:1234/v1
- name: "LM Studio"
- params:
- reasoning_effort: medium
-
- glm-4-7-flash:
- provider: openai_compatible
- model: zai-org/glm-4.7-flash
- base_url: http://127.0.0.1:1234/v1
- name: "LM Studio"
-
- nemotron-3-nano-4b:
- provider: openai_compatible
- model: nvidia/nemotron-3-nano-4b
- base_url: http://127.0.0.1:1234/v1
- name: "LM Studio"
-
- lm-glm4v:
- provider: openai_compatible
- model: zai-org/glm-4.6v-flash
- base_url: http://127.0.0.1:1234/v1
- name: "LM Studio"
- qwen3:
- provider: openai_compatible
- model: qwen/qwen3-vl-30b
- base_url: http://127.0.0.1:1234/v1
- name: "LM Studio"
- gpt-oss-20b:
- provider: openai_compatible
- model: openai/gpt-oss-20b
- base_url: http://127.0.0.1:1234/v1
- name: "LM Studio"
-
-tools:
- # - agent
- - skills
- - files_list
- - files_read
- - files_write
- # - files_edit
- - files_replace
- - docs_help_page
- - docs_package_help_topics
-
-options:
- skills:
- paths: ["~/.agents/skills"]
- subagent:
- # client: openai/gpt-5.4-mini
- tools_allowed: [docs, files_search, files_list]
----
-
-## Overview
-
-btw is an R package that helps humans and LLMs work together with R by providing utilities to describe R objects, package documentation, and workspace state in LLM-friendly plain text. The package offers a flexible collection of tools that can be used interactively (copy-paste workflows), programmatically (direct function calls), or as enhanced chat clients (via ellmer or MCP servers).
-
-The primary goal is creating a collection of tools useful to both LLMs and humans when working together with R, with an emphasis on flexibility of usage across different workflows and platforms.
-
-## Quick Reference
-
-- **Project type:** R Package
-- **Language:** R (≥ 4.1.0)
-- **Key frameworks:** ellmer (LLM chat integration), mcptools (Model Context Protocol), shiny and shinychat (chat app)
-
-## Purpose and Design Philosophy
-
-btw prioritizes flexibility of usage through multiple entry points:
-
-- **`btw()`** - Interactive copy-paste workflow: gather context from R and paste into any chat interface
-- **`btw_tools()`** - Register tools with ellmer chat clients for custom applications
-- **`btw_client()` / `btw_app()`** - Batteries-included chat clients with your preferred LLM provider, model, and project context
-- **MCP server** - Expose tools to third-party coding agents like Claude Desktop or Continue via `btw_mcp_server()`
-
-Project configuration via `btw.md` files provides conversation stability across sessions by defining default provider, model, tools, and project-specific instructions. These files are treated as instructions for coding assistants and help avoid repeating context.
-
-btw also serves as a laboratory for discovering best practices in LLM tool design - output formats and approaches evolve based on experimentation with what works best across different models.
\ No newline at end of file
diff --git a/inst/icons/ink-eraser.svg b/inst/icons/ink-eraser.svg
new file mode 100644
index 00000000..8969a7fe
--- /dev/null
+++ b/inst/icons/ink-eraser.svg
@@ -0,0 +1 @@
+
diff --git a/inst/js/app/btw_app.css b/inst/js/app/btw_app.css
index 538bb6c9..1a25352d 100644
--- a/inst/js/app/btw_app.css
+++ b/inst/js/app/btw_app.css
@@ -66,15 +66,23 @@ shiny-chat-message .message-icon {
height: 1em;
width: 1em;
}
-.status-provider-model {
- text-overflow: ellipsis;
- text-wrap-mode: nowrap;
- overflow: hidden;
+.status-footer .status-tokens,
+.status-footer .status-cost {
+ display: inline-flex;
+ gap: 0.75rem;
+}
+.status-footer .status-cost {
+ margin-left: 0.5rem;
}
.modal {
--bs-modal-margin: min(2rem, 10%);
}
+.status-provider.badge {
+ color: var(--bs-body-color);
+ background-color: rgba(var(--bs-body-color-rgb), 0.1);
+}
+
/* ---------------- Tool Results ---------------- */
.btw-tool-result-write-file > .card > .card-body {
padding: 0;
diff --git a/inst/js/app/btw_app.js b/inst/js/app/btw_app.js
index fc06c84a..2e2a9412 100644
--- a/inst/js/app/btw_app.js
+++ b/inst/js/app/btw_app.js
@@ -20,11 +20,30 @@ function initializeCountUp(element, initialValue, options) {
return counter
}
-document.addEventListener("DOMContentLoaded", function () {
- document.querySelectorAll(".status-countup").forEach((element) => {
+function initializeStatusCountups() {
+ const elements = document.querySelectorAll(".status-countup")
+ elements.forEach((element) => {
+ if (statusCounters.has(element)) return
const counter = initializeCountUp(element, 0)
statusCounters.set(element, counter)
})
+ return elements.length > 0
+}
+
+document.addEventListener("DOMContentLoaded", function () {
+ if (initializeStatusCountups()) return
+
+ let attempt = 0
+ const maxAttempts = 5
+ const baseDelay = 100
+
+ function retry() {
+ attempt++
+ if (initializeStatusCountups() || attempt >= maxAttempts) return
+ setTimeout(retry, baseDelay * Math.pow(2, attempt))
+ }
+
+ setTimeout(retry, baseDelay)
})
if (typeof Shiny !== "undefined") {
@@ -32,11 +51,12 @@ if (typeof Shiny !== "undefined") {
const element = document.getElementById(message.id)
if (element) {
const counter = statusCounters.get(element)
- const lastValue = parseFloat(element.dataset.value | "0")
+ const lastValue = parseFloat(element.dataset.value || "0")
- if (counter && message.value) {
+ if (counter && Object.hasOwn(message, "value")) {
if (
element.dataset.type === "cost" &&
+ message.value > 0 &&
(lastValue < 0.1 || message.value < 0.1)
) {
const newCounter = initializeCountUp(element, lastValue, {
@@ -61,6 +81,19 @@ if (typeof Shiny !== "undefined") {
}
}
})
+
+ Shiny.addCustomMessageHandler("btw_reset_status", function (message) {
+ const elements = document.querySelectorAll(".status-countup")
+ elements.forEach((element) => {
+ if (!element.id.startsWith(message.ns)) return
+ element.classList.remove("btw-status-recalculating", "btw-status-unknown")
+ element.dataset.value = 0
+ const counter = statusCounters.get(element)
+ if (counter) {
+ counter.update(0)
+ }
+ })
+ })
}
// Open File Buttons ----------------------------------------------------------
diff --git a/man/btw_client.Rd b/man/btw_client.Rd
index 9391d9d3..dfa06260 100644
--- a/man/btw_client.Rd
+++ b/man/btw_client.Rd
@@ -13,7 +13,14 @@ btw_client(
path_llms_txt = NULL
)
-btw_app(..., client = NULL, tools = NULL, path_btw = NULL, messages = list())
+btw_app(
+ ...,
+ client = NULL,
+ tools = NULL,
+ path_btw = NULL,
+ messages = list(),
+ model_choices = c("auto", "btw_md", "provider", "none")
+)
}
\arguments{
\item{...}{In \code{btw_app()}, additional arguments are passed to
@@ -54,6 +61,11 @@ the your current working directory or its parents. Set \code{path_llms_txt = FAL
\item{messages}{A list of initial messages to show in the chat, passed to
\code{\link[shinychat:chat_mod_ui]{shinychat::chat_mod_ui()}}.}
+
+\item{model_choices}{Can be one of \code{"btw_md"} (model choices from your
+\code{path_btw} configuration), \code{"provider"} (models from the provider API),
+\code{"auto"} (uses \code{path_btw} if \code{client} comes from \code{path_btw}, otherwise
+falling back to provider), or \code{"none"} (don't show model choices).}
}
\value{
Returns an \link[ellmer:Chat]{ellmer::Chat} object with additional tools registered