Changes in version 0.11.1 (2026-07-07) New features - Linked plotly selection helpers (ADR 0028). Two new exports turn the state returned by ptr_server() into an interactive, brushable plot. ptr_ggplotly() converts the state's current ggplot into a plotly htmlwidget — minting the per-row selection keys and wiring the crosstalk source internally, so the call is state-first (ptr_ggplotly(state)). ptr_plotly_selection() returns a reactive of the rows the user brushed or clicked on that plot (mode = "rows" and other projections), reset on each draw. Together they support a full brush-to-table loop. See ?ptr_ggplotly and ?ptr_plotly_selection. - ptr_options(gate_draw = ) toggles the "Update plot" button. When TRUE (the default) the plot re-renders only on the button click, batching placeholder changes; when FALSE the button is omitted and the plot re-renders reactively on every placeholder change (live mode). It also governs the grid host's "Draw all" button. Read once when the app is built, so set it before ptr_app() / ptr_ui(). - ptr_options(suppress_warnings = ) silences R warnings emitted while the plot is drawn (e.g. loess "all data on boundary of neighborhood" / "Failed to fit group N"). Default FALSE. Only the plot-drawing step is wrapped; errors still propagate to the inline error pane. Bug fixes - Runtime ppExpr safety messages are now truthful — when a runtime ppExpr input is rejected, the abort reports the actual reason the expression was blocked rather than a generic message. - The LLM docs tool argument was renamed name → topic. The shipped primer instructs models to call ggpaintr_docs(topic), but the registered ellmer tool schema (and ptr_llm_topic()'s parameter) named the argument name, so a model following the primer emitted a key the schema rejected. The schema argument and ptr_llm_topic() now agree on topic. Documentation - The safety and LLM vignettes were migrated to the ggpaintr book (https://willju-wangqian.github.io/ggpaintr-book/); the package ships the ggpaintr-tutorial vignette as its single built vignette. Changes in version 0.11.0 Breaking changes (ADR 0027) - The four public placeholder-constructor arguments were renamed across ptr_define_placeholder_value() / ptr_define_placeholder_consumer() / ptr_define_placeholder_source(): positional_arg → parse_positional_arg, named_args → parse_named_args, validate_input → validate_session_input, and runtime → embellish_eval. The rename makes the slot family legible — two slots parse the formula's default args into the widget seed (parse_positional_arg, parse_named_args), one validates the live session value (validate_session_input), and one defines the plain-R evaluation of the embellished expression (embellish_eval). This is a hard break with no deprecation shim: passing an old name now raises R's "unused argument" error. The internal registry-record keys are unchanged (entry$default_arg / named_args / validate_input / runtime); only the public argument names changed. - embellish_eval defaults to the new embellish_identity() for value/consumer placeholders (the source role keeps its abort-guard default). Two built-in helpers ship for the plain-R evaluation slot: embellish_identity() (the identity function(x, ...) x) and embellish_symbol_to_string() (NSE-captures its argument and returns column names as a character vector, so a column-selecting consumer works inside a tidyselect verb when the placeholder-embellished formula is run as ordinary R with no app). - ptr_arg_numeric_vector() was removed. Use ptr_arg_numeric(vector = TRUE). The argument-validator factories (ptr_arg_numeric / ptr_arg_string / ptr_arg_symbol_or_string) gained a vector = FALSE flag (with length = honored in vector mode for ptr_arg_numeric), and a new ptr_arg_symbol() factory parses a bareword column name (vector-capable) for multi-column positional defaults. - ptr_app_bslib() and ptr_app_grid() are no longer exported (changed to @keywords internal). They remain available internally and their help pages are still generated so existing cross-reference links resolve, but they are no longer presented as public API; the public turn-key entry point is ptr_app(). Write a thin wrapper on the public primitives for a custom page shell or theme. New features - RStudio command-palette addin. ptr_wrap_placeholder_addin() wraps a highlighted selection in a placeholder from the RStudio command palette (classifies the selection, offers a placeholder picker, rewrites in place; dark/light-aware via rstudioapi::getThemeInfo()). - Documentation now leads with the expression form. Roxygen @examples for the formula-capturing entry points (ptr_app, ptr_ui_controls, ptr_ui_page, ptr_server) lead with the unquoted ggplot expression (the canonical input), keeping the string form as the documented fallback; Shiny examples are gated with if (interactive()). Bug fixes (formula / ppExpr codegen) - A ~ formula is treated as a first-class atomic in the canonical-pipeline lift, so the resugar pass no longer shreds a formula inside ppExpr (e.g. lm(ppExpr(y ~ x), data = ppUpload(d))); placeholders inside the formula are still detected and wired because translate_call() recurses into every argument independently. - A two-sided formula renders with its infix ~ (gear ~ cyl) instead of falling through to a prefix ~(gear, cyl), and an explicit parenthesis node (x) now renders as a balanced (x) instead of the unparseable ((x). Changes in version 0.10.0 New features - ptr_shared() now accepts quoted ggplot expressions as formulas elements, alongside strings. Each element of formulas may be a formula string (as before) or a quoted expression built with rlang::expr() / quote(); quoted expressions are deparsed to their source and the two forms are interchangeable, including in a single mixed list. This brings the multi-formula coordinator in line with the string-or-expression input already accepted by ptr_app() / ptr_server(). A built ggplot object (whose source text is unrecoverable) is rejected with a message pointing at expr(). Additive and fully backward-compatible — existing all-string call sites are unchanged. (As with the single-formula entries, a native pipe |> inside a quoted expression is desugared by R before capture and so does not survive into the generated code panel.) Breaking changes - Source surface rename + auto-name + UI mutex + code prologue (ADR 0025). ptr_define_placeholder_source() no longer accepts a companion_id_fn = function(id) -> string callback; pass shortcut = TRUE (single logical, default FALSE) to opt into the env-shortcut sibling input. The framework stamps node$shortcut_id <- paste0(node$id, "_shortcut") on every translated source node (the old node$companion_id slot and the "_name" suffix are gone). Built-in ppUpload migrates verbatim. The reserved shared key "shortcut" aborts translation with a named-conflict error. Hard rename, no lifecycle::deprecate_warn() shim. The same surface ships four further behaviours: - Auto-name (§2). Every source node now carries node$auto_name, stamped at translate time for non-shared sources (node$default %||% node$id) and at runtime for shared sources (paste0(obj$id, "_", key)). When the shortcut textbox is empty, substitute_pass splices as.name(node$auto_name) so the rendered call always references a real binding. - UI mutex (§3). For ppUpload, picking a file synchronously clears the shortcut textbox (the auto-name takes over as the binding name); typing into the textbox synchronously clears any pending file. Typing always means "look up this object in the caller env", never "rename the uploaded payload". - Code-panel prologue (§4). Every active upload emits a one-line prologue at the top of the code panel — <- read.csv("") (or read.delim, readxl::read_excel, jsonlite::fromJSON, … per extension) — so the rendered snippet reads top-to-bottom as a self-contained script. - Vacate-on-empty + spec round-trip (§5, §7 A1). Clearing the shortcut textbox with no file present synchronously vacates state$bound_names[[key]], state$eval_env[[key]], and state$active_uploads[[key]]. ptr_spec() dumps node$auto_name (not "") as the bound name whenever the textbox is empty, so reloading a spec re-establishes the same binding the running app saw. Bug fixes — ADR 0025 follow-ups - Two ptr_shared(..., id = ...) coordinators on one page sharing a panel-owned source key no longer collide on bare DOM ids. When a panel-shared ppUpload(shared = '') was rendered under a coordinator with a non-NULL id, the inner fileInput + shortcut textInput still bound at the bare shared_ / shared__shortcut ids — so a second coordinator (id = "right") on the same page emitted duplicate ids and silently failed to bind one of the two uploads. The rendered widget now binds at the coordinator-namespaced ids (left-shared_ / right-shared_), matching the sibling render paths in invoke_build_ui() and ptr_setup_source_uis(). Single-coordinator apps (no id = supplied) are byte-for-byte unchanged. Known limitation — ADR 0025 §7 A2 deferred (400 ms shortcut debounce) ptr_define_placeholder_source(shortcut = TRUE) reads the shortcut textbox at every keystroke. Two implementation attempts at the ADR's proposed 400 ms debounce (shiny::debounce() on the shortcut input read) both broke unrelated test contracts — the bind-path delay broke test-rewrite-pipeline-data-source.R (6 FAIL) and the mutex-file-reset race in test-shared-source-panel-multi-instance.R (2 FAIL). The debounce is therefore not shipped in this release. User-visible impact: typing a partial, unresolvable name into the shortcut textbox may briefly surface a transient Object 'm' not found-style error in the inline error pane during the typing burst; the bind settles once the user finishes typing. A debounce-with-race-preservation variant is tracked as a follow-up; A1 (vacate-on-empty) is unaffected and ships in this release. - validate_input hook signature unified across value and consumer placeholders. Both ptr_define_placeholder_value() and ptr_define_placeholder_consumer() now accept validate_input = function(value, ctx). The consumer's previous function(value, upstream_cols) signature is gone — registrations that still use it fail loudly via validate_hook() ("must accept argument(s): ctx"). ctx is a plain list with named fields: node (the placeholder AST node), keyword (alias for node$keyword), upstream_cols (character vector or NULL), and data (the upstream data frame or NULL). For value placeholders, ctx$upstream_cols and ctx$data are always NULL (value placeholders have no upstream column scope by definition); for consumer placeholders, both fields are populated when the upstream resolves, letting data-aware validators inspect column types / ranges / levels and not just column names. Migration: rename the second formal from upstream_cols to ctx and read ctx$upstream_cols in the body. Value placeholders that previously had no validator gain the option to declare one. Breaking changes (ADR 0009) - Placeholder keywords renamed from var / num / text / expr / upload to ppVar / ppNum / ppText / ppExpr / ppUpload. Every formula passed to ptr_app() / ptr_server() / ptr_translate() must use the new vocabulary. The pp-prefix avoids shadowing base R / ggplot2 names (stats::var(), graphics::text(), base::expression, etc.) and lets the same symbols double as identity / guard plain-R functions exported from the package — so ggplot(mtcars, aes(x = ppVar(mpg))) works both inside ptr_app() (as a placeholder) and as a standalone ggplot call (ppVar is identity, so aes(x = ppVar(mpg)) evaluates to aes(x = mpg)). See ADR 0009 for the full rationale. Migration: rename every placeholder call inside your formula strings or unquoted-expression formulas. - Old keyword names are no longer reserved. The names var, num, text, expr, upload are no longer registered by ggpaintr and no longer trigger the shadow-check guard, so users who previously had custom placeholders blocked by built-ins of these names can now register them without conflict. - Shiny input ids changed. ggpaintr's internal Shiny input ids are not part of the public API; the keyword rename has shifted them (e.g. ggplot_1_1_var_NA is now ggplot_1_1_ppVar_NA). Bookmarked URLs / setBookmarkExclude allow-lists / setInputs() test fixtures from previous versions will need to be updated. New features - Default-value arguments and code-mode toggle (ADR 0009). Built-in placeholders now accept a positional argument that seeds the widget's initial value: ppVar(mpg) opens the column picker pre-selected on mpg, ppNum(5) seeds the numeric input at 5, ppText("hello") pre-fills the text input, and ppExpr(factor(cyl)) pre-fills the expression box. Defaults survive the rename round-trip: ptr_render(root, preserve_placeholders = TRUE) re-emits the original ppX(default) call rather than the substituted value. The ptr_app() code panel gained a "Final code" / "Show placeholders" toggle that switches between the two render modes. - Custom placeholders can opt in to the default/named-args schema. ptr_define_placeholder_value() / ptr_define_placeholder_consumer() / ptr_define_placeholder_source() now accept default_arg = and named_args = list( = , ...). Use the bundled validator factories — ptr_default_symbol_or_string(), ptr_default_numeric(), ptr_default_numeric_vector(), ptr_default_string(), ptr_default_expression() — or pass a custom closure. See ?ptr_define_placeholder_value. - L3 — own every piece of the UI. Every piece of ggpaintr's public UI now has its own exported builder: ptr_ui_header(), ptr_ui_controls(), ptr_ui_plot(), ptr_ui_error(), ptr_ui_code(), ptr_ui_code_toggle(), and ptr_ui_assets(). Compose exactly the pieces you want, place each anywhere in your own layout, and wire them with the existing server API (ptr_server() / ptr_module_server()). The bundled ptr_app() / ptr_module_ui() / ptr_outputs_ui() / ptr_controls_ui() paths are reimplemented as thin composites of these pieces — same DOM, no behavior or performance change. The pieces are deliberately bare (no assets, no .ptr-app wrapper); the new ptr_ui_page() shell wraps them in a Bootstrap page + the single .ptr-app theme scope + the (deduped) asset bundle in one call, so that is all an L3 user has to remember. Swap the page builder with page = (fluidPage default, also fixedPage/fillPage/bootstrapPage/basicPage); for a navbarPage or bslib root use the documented decomposition recipe. See vignette("ggpaintr-use-cases") § "L3 — Own every piece of the UI". - ptr_server() now accepts a ptr_shared_server() bundle directly via shared_state =, mirroring ptr_module_server(). Wiring a page-level ptr_shared_ui() panel to a single embedded or custom-rendered plot no longer requires spreading the four bundle slots through .... - The custom-renderer pattern (reading state$runtime() for your own renderPlotly() / renderGirafe()) is now documented as an L2 capability — both ptr_server() and ptr_module_server() return the ptr_state, so it needs nothing beyond embedding. Behavior changes - ptr_register_plot() / ptr_register_error() / ptr_register_code() are no longer exported. Post-rewrite they only ever ran inside ptr_server() (their sole caller, via the internal runtime setup) and rendered nothing without it, so the "compose outputs manually" use case they advertised was impossible. They remain as internal helpers; no replacement is needed (use the L3 pieces + ptr_server(), or read state$runtime() for a custom renderer). No deprecation cycle — this dev cycle's own surface, single pre-1.0 user. - ggpaintr's CSS/JS now ship as deduped htmltools::htmlDependency() bundles. ggpaintr.css, the code-window JS, and the ptr_set_class handler + stage CSS are emitted as two dependencies (ggpaintr, ggpaintr-layer) instead of inline